消息模块、爬虫
This commit is contained in:
145
schoolNewsServ/.bin/mysql/sql/createTableMessage.sql
Normal file
145
schoolNewsServ/.bin/mysql/sql/createTableMessage.sql
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- 消息通知模块 - 数据库表结构
|
||||||
|
-- 包含3张表:消息主体表、消息接收对象表、用户消息表
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- 1. 消息主体表
|
||||||
|
DROP TABLE IF EXISTS tb_sys_message;
|
||||||
|
CREATE TABLE tb_sys_message (
|
||||||
|
-- 基础标识
|
||||||
|
id VARCHAR(50) PRIMARY KEY COMMENT '主键ID',
|
||||||
|
message_id VARCHAR(50) NOT NULL UNIQUE COMMENT '消息唯一标识',
|
||||||
|
|
||||||
|
-- 消息内容
|
||||||
|
title VARCHAR(200) NOT NULL COMMENT '消息标题',
|
||||||
|
content TEXT NOT NULL COMMENT '消息内容',
|
||||||
|
message_type VARCHAR(20) NOT NULL DEFAULT 'notification' COMMENT '消息类型:notification-通知/announcement-公告/warning-预警',
|
||||||
|
priority VARCHAR(20) NOT NULL DEFAULT 'normal' COMMENT '优先级:normal-普通/important-重要/urgent-紧急',
|
||||||
|
|
||||||
|
-- 发送人信息
|
||||||
|
sender_id VARCHAR(50) NOT NULL COMMENT '发送人用户ID',
|
||||||
|
sender_name VARCHAR(100) COMMENT '发送人姓名(冗余字段)',
|
||||||
|
sender_dept_id VARCHAR(50) NOT NULL COMMENT '发送人部门ID',
|
||||||
|
sender_dept_name VARCHAR(100) COMMENT '发送人部门名称(冗余字段)',
|
||||||
|
|
||||||
|
-- 发送时间控制(定时发送功能)
|
||||||
|
send_mode VARCHAR(20) NOT NULL DEFAULT 'immediate' COMMENT '发送模式:immediate-立即发送/scheduled-定时发送',
|
||||||
|
scheduled_time DATETIME COMMENT '计划发送时间(sendMode=scheduled时必填)',
|
||||||
|
actual_send_time DATETIME COMMENT '实际发送时间',
|
||||||
|
|
||||||
|
-- 状态管理
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT '状态:draft-草稿/pending-待发送/sending-发送中/sent-已发送/failed-失败/cancelled-已取消',
|
||||||
|
|
||||||
|
-- 统计信息
|
||||||
|
target_user_count INT DEFAULT 0 COMMENT '目标用户总数',
|
||||||
|
sent_count INT DEFAULT 0 COMMENT '已发送数量',
|
||||||
|
success_count INT DEFAULT 0 COMMENT '发送成功数量',
|
||||||
|
failed_count INT DEFAULT 0 COMMENT '发送失败数量',
|
||||||
|
read_count INT DEFAULT 0 COMMENT '已读数量',
|
||||||
|
|
||||||
|
-- 失败处理和重试机制
|
||||||
|
retry_count INT DEFAULT 0 COMMENT '当前重试次数',
|
||||||
|
max_retry_count INT DEFAULT 3 COMMENT '最大重试次数',
|
||||||
|
last_error TEXT COMMENT '最后错误信息',
|
||||||
|
|
||||||
|
-- 基础字段
|
||||||
|
creator VARCHAR(50) COMMENT '创建人ID',
|
||||||
|
updater VARCHAR(50) COMMENT '更新人ID',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
delete_time DATETIME COMMENT '删除时间',
|
||||||
|
deleted BIT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
INDEX idx_message_id (message_id),
|
||||||
|
INDEX idx_sender_id (sender_id),
|
||||||
|
INDEX idx_sender_dept_id (sender_dept_id),
|
||||||
|
INDEX idx_status (status),
|
||||||
|
INDEX idx_send_mode (send_mode),
|
||||||
|
INDEX idx_scheduled_time (scheduled_time),
|
||||||
|
INDEX idx_create_time (create_time),
|
||||||
|
INDEX idx_delete_time (delete_time)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='消息主体表';
|
||||||
|
|
||||||
|
|
||||||
|
-- 2. 消息发送方式接收对象表
|
||||||
|
DROP TABLE IF EXISTS tb_sys_message_target;
|
||||||
|
CREATE TABLE tb_sys_message_target (
|
||||||
|
-- 基础标识
|
||||||
|
id VARCHAR(50) PRIMARY KEY COMMENT '主键ID',
|
||||||
|
message_id VARCHAR(50) NOT NULL COMMENT '消息ID(关联tb_sys_message.message_id)',
|
||||||
|
|
||||||
|
-- 发送方式
|
||||||
|
send_method VARCHAR(100) NOT NULL COMMENT '发送方式:system-系统消息/email-邮件/sms-短信(多选时逗号分隔,如:system,email)',
|
||||||
|
|
||||||
|
-- 接收对象
|
||||||
|
target_type VARCHAR(20) NOT NULL COMMENT '接收对象类型:dept-部门/role-角色/user-人员',
|
||||||
|
target_id VARCHAR(50) NOT NULL COMMENT '接收对象ID(部门ID/角色ID/用户ID)',
|
||||||
|
target_name VARCHAR(100) COMMENT '接收对象名称(冗余字段,便于展示)',
|
||||||
|
|
||||||
|
-- 作用域部门(关键字段:限制角色的部门范围)
|
||||||
|
scope_dept_id VARCHAR(50) NOT NULL COMMENT '作用域部门ID:dept时与target_id相同;role时表示该角色限定在哪个部门及其子部门范围内;user时为用户所属部门ID',
|
||||||
|
|
||||||
|
-- 基础字段
|
||||||
|
creator VARCHAR(50) COMMENT '创建人ID',
|
||||||
|
updater VARCHAR(50) COMMENT '更新人ID',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
delete_time DATETIME COMMENT '删除时间',
|
||||||
|
deleted tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
INDEX idx_messageID (message_id),
|
||||||
|
INDEX idx_target_type (target_type),
|
||||||
|
INDEX idx_scope_dept_id (scope_dept_id),
|
||||||
|
INDEX idx_delete_time (delete_time)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='消息发送方式接收对象表';
|
||||||
|
|
||||||
|
|
||||||
|
-- 3. 用户接收消息表
|
||||||
|
DROP TABLE IF EXISTS tb_sys_message_user;
|
||||||
|
CREATE TABLE tb_sys_message_user (
|
||||||
|
-- 基础标识
|
||||||
|
id VARCHAR(50) PRIMARY KEY COMMENT '主键ID',
|
||||||
|
message_id VARCHAR(50) NOT NULL COMMENT '消息ID(关联tb_sys_message.message_id)',
|
||||||
|
user_id VARCHAR(50) NOT NULL COMMENT '接收用户ID',
|
||||||
|
|
||||||
|
-- 发送方式
|
||||||
|
send_method VARCHAR(20) NOT NULL COMMENT '实际发送方式:system-系统消息/email-邮件/sms-短信',
|
||||||
|
|
||||||
|
-- 阅读状态
|
||||||
|
is_read tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已读:0-未读,1-已读',
|
||||||
|
read_time DATETIME COMMENT '阅读时间',
|
||||||
|
|
||||||
|
-- 发送状态
|
||||||
|
send_status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '发送状态:pending-待发送/success-发送成功/failed-发送失败',
|
||||||
|
fail_reason TEXT COMMENT '失败原因',
|
||||||
|
|
||||||
|
-- 基础字段
|
||||||
|
creator VARCHAR(50) COMMENT '创建人ID',
|
||||||
|
updater VARCHAR(50) COMMENT '更新人ID',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
delete_time DATETIME COMMENT '删除时间',
|
||||||
|
deleted tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
|
||||||
|
|
||||||
|
-- 索引
|
||||||
|
INDEX idx_message_id (message_id),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_is_read (is_read),
|
||||||
|
INDEX idx_send_status (send_status),
|
||||||
|
INDEX idx_deleted (deleted),
|
||||||
|
INDEX idx_user_read (user_id, is_read, deleted) COMMENT '用户未读消息查询索引'
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户接收消息表';
|
||||||
|
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 初始化说明
|
||||||
|
-- =====================================================
|
||||||
|
-- 1. 执行本SQL脚本创建3张表
|
||||||
|
-- 2. 执行message_menu.sql插入菜单权限数据
|
||||||
|
-- 3. 表结构说明:
|
||||||
|
-- - tb_sys_message:存储消息主体信息,支持定时发送
|
||||||
|
-- - tb_sys_message_target:存储接收对象配置,scopeDeptID限制权限范围
|
||||||
|
-- - tb_sys_message_user:存储每个用户的消息记录,支持已读/未读状态
|
||||||
|
-- =====================================================
|
||||||
@@ -39,6 +39,8 @@ SOURCE createTableAchievement.sql;
|
|||||||
|
|
||||||
SOURCE createTableCrontab.sql;
|
SOURCE createTableCrontab.sql;
|
||||||
|
|
||||||
|
SOURCE createTableMessage.sql;
|
||||||
|
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
-- 插入初始数据
|
-- 插入初始数据
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ INSERT INTO `tb_sys_module` (id, module_id, name, code, description, icon, order
|
|||||||
('4', 'module_ai', 'AI管理', 'ai', 'AI管理模块', 'el-icon-cpu', 4, 1, '1', now()),
|
('4', 'module_ai', 'AI管理', 'ai', 'AI管理模块', 'el-icon-cpu', 4, 1, '1', now()),
|
||||||
('5', 'module_usercenter', '用户中心', 'usercenter', '用户中心模块', 'el-icon-user', 5, 1, '1', now()),
|
('5', 'module_usercenter', '用户中心', 'usercenter', '用户中心模块', 'el-icon-user', 5, 1, '1', now()),
|
||||||
('6', 'module_file', '文件管理', 'file', '文件管理模块', 'el-icon-folder', 6, 1, '1', now()),
|
('6', 'module_file', '文件管理', 'file', '文件管理模块', 'el-icon-folder', 6, 1, '1', now()),
|
||||||
('7', 'module_crontab', '定时任务', 'crontab', '定时任务管理模块', 'el-icon-alarm-clock', 7, 1, '1', now());
|
('7', 'module_crontab', '定时任务', 'crontab', '定时任务管理模块', 'el-icon-alarm-clock', 7, 1, '1', now()),
|
||||||
|
('8', 'module_message', '消息通知', 'message', '消息通知管理模块', 'el-icon-message', 8, 1, '1', now());
|
||||||
|
|
||||||
-- 插入权限数据
|
-- 插入权限数据
|
||||||
INSERT INTO `tb_sys_permission` (id,permission_id, name, code, description, module_id, creator, create_time) VALUES
|
INSERT INTO `tb_sys_permission` (id,permission_id, name, code, description, module_id, creator, create_time) VALUES
|
||||||
@@ -60,7 +61,10 @@ INSERT INTO `tb_sys_permission` (id,permission_id, name, code, description, modu
|
|||||||
('12','perm_usercenter_manage', '用户中心管理', 'usercenter:manage', '用户中心管理权限', 'module_usercenter', '1', now()),
|
('12','perm_usercenter_manage', '用户中心管理', 'usercenter:manage', '用户中心管理权限', 'module_usercenter', '1', now()),
|
||||||
('13','perm_file_manage', '文件管理', 'file:manage', '文件管理权限', 'module_file', '1', now()),
|
('13','perm_file_manage', '文件管理', 'file:manage', '文件管理权限', 'module_file', '1', now()),
|
||||||
('14','perm_crontab_manage', '定时任务管理', 'crontab:manage', '定时任务管理权限', 'module_crontab', '1', now()),
|
('14','perm_crontab_manage', '定时任务管理', 'crontab:manage', '定时任务管理权限', 'module_crontab', '1', now()),
|
||||||
('15','perm_crontab_execute', '定时任务执行', 'crontab:execute', '定时任务执行权限', 'module_crontab', '1', now());
|
('15','perm_crontab_execute', '定时任务执行', 'crontab:execute', '定时任务执行权限', 'module_crontab', '1', now()),
|
||||||
|
('16','perm_message_manage', '消息管理', 'message:manage', '消息管理权限(管理端)', 'module_message', '1', now()),
|
||||||
|
('17','perm_message_send', '消息发送', 'message:send', '消息发送权限', 'module_message', '1', now()),
|
||||||
|
('18','perm_message_view', '消息查看', 'message:view', '消息查看权限(用户端)', 'module_message', '1', now());
|
||||||
|
|
||||||
-- 插入角色-权限关联数据
|
-- 插入角色-权限关联数据
|
||||||
INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, create_time) VALUES
|
INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, create_time) VALUES
|
||||||
@@ -82,6 +86,9 @@ INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, creat
|
|||||||
('15', 'superadmin', 'perm_file_manage', '1', now()),
|
('15', 'superadmin', 'perm_file_manage', '1', now()),
|
||||||
('16', 'superadmin', 'perm_crontab_manage', '1', now()),
|
('16', 'superadmin', 'perm_crontab_manage', '1', now()),
|
||||||
('17', 'superadmin', 'perm_crontab_execute', '1', now()),
|
('17', 'superadmin', 'perm_crontab_execute', '1', now()),
|
||||||
|
('18', 'superadmin', 'perm_message_manage', '1', now()),
|
||||||
|
('19', 'superadmin', 'perm_message_send', '1', now()),
|
||||||
|
('19.1', 'superadmin', 'perm_message_view', '1', now()),
|
||||||
|
|
||||||
-- 管理员:拥有业务管理权限,但没有系统日志等系统管理权限
|
-- 管理员:拥有业务管理权限,但没有系统日志等系统管理权限
|
||||||
('20', 'admin', 'perm_default', '1', now()),
|
('20', 'admin', 'perm_default', '1', now()),
|
||||||
@@ -92,13 +99,17 @@ INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, creat
|
|||||||
('25', 'admin', 'perm_ai_manage', '1', now()),
|
('25', 'admin', 'perm_ai_manage', '1', now()),
|
||||||
('26', 'admin', 'perm_usercenter_manage', '1', now()),
|
('26', 'admin', 'perm_usercenter_manage', '1', now()),
|
||||||
('27', 'admin', 'perm_file_manage', '1', now()),
|
('27', 'admin', 'perm_file_manage', '1', now()),
|
||||||
|
('28', 'admin', 'perm_message_manage', '1', now()),
|
||||||
|
('29', 'admin', 'perm_message_send', '1', now()),
|
||||||
|
('29.1', 'admin', 'perm_message_view', '1', now()),
|
||||||
|
|
||||||
-- 自由角色:拥有用户视图相关的所有权限(前台用户权限)
|
-- 自由角色:拥有用户视图相关的所有权限(前台用户权限)
|
||||||
('30', 'freedom', 'perm_default', '1', now()),
|
('30', 'freedom', 'perm_default', '1', now()),
|
||||||
('31', 'freedom', 'perm_news_article_add', '1', now()),
|
('31', 'freedom', 'perm_news_article_add', '1', now()),
|
||||||
('32', 'freedom', 'perm_ai_manage', '1', now()),
|
('32', 'freedom', 'perm_ai_manage', '1', now()),
|
||||||
('33', 'freedom', 'perm_usercenter_manage', '1', now()),
|
('33', 'freedom', 'perm_usercenter_manage', '1', now()),
|
||||||
('34', 'freedom', 'perm_file_manage', '1', now());
|
('34', 'freedom', 'perm_file_manage', '1', now()),
|
||||||
|
('35', 'freedom', 'perm_message_view', '1', now());
|
||||||
|
|
||||||
-- 插入前端菜单数据
|
-- 插入前端菜单数据
|
||||||
INSERT INTO `tb_sys_menu` VALUES
|
INSERT INTO `tb_sys_menu` VALUES
|
||||||
@@ -106,7 +117,7 @@ INSERT INTO `tb_sys_menu` VALUES
|
|||||||
('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),
|
('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),
|
||||||
('101', 'menu_resource_hot', '热门资源', NULL, '/resource-hot', 'user/resource-center/HotResourceView', NULL, 2, 3, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:49:56', NULL, 0),
|
('101', 'menu_resource_hot', '热门资源', NULL, '/resource-hot', 'user/resource-center/HotResourceView', NULL, 2, 3, '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),
|
('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),
|
('300', 'menu_study_plan', '学习计划', NULL, '/study-plan', '', 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),
|
('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),
|
('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),
|
('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),
|
||||||
@@ -120,7 +131,6 @@ INSERT INTO `tb_sys_menu` VALUES
|
|||||||
('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),
|
('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),
|
('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),
|
('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-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),
|
('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),
|
('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),
|
||||||
@@ -157,7 +167,12 @@ INSERT INTO `tb_sys_menu` VALUES
|
|||||||
('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),
|
('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),
|
('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),
|
('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);
|
('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),
|
||||||
|
-- 消息通知模块菜单 (9000-9999)
|
||||||
|
('9001', 'menu_admin_message_manage', '消息管理', NULL, '/admin/manage/message', 'admin/manage/message/MessageManageView', 'admin/message.svg', 9, 0, 'SidebarLayout', '1', NULL, '2025-11-13 10:00:00', '2025-11-13 10:00:00', NULL, 0),
|
||||||
|
-- 用户端消息中心菜单 (650-699)
|
||||||
|
('650', 'menu_user_message_center', '消息中心', NULL, '/user/message', 'user/message/MyMessageListView', NULL, 7, 1, 'NavigationLayout', '1', NULL, '2025-11-13 10:00:00', '2025-11-13 10:00:00', NULL, 0),
|
||||||
|
('651', 'menu_user_message_detail', '消息详情', 'menu_user_message_center', '/user/message/detail/:messageID', 'user/message/MyMessageDetailView', NULL, 1, 3, 'NavigationLayout', '1', NULL, '2025-11-13 10:00:00', '2025-11-13 10:00:00', NULL, 0);
|
||||||
-- 插入菜单权限关联数据
|
-- 插入菜单权限关联数据
|
||||||
INSERT INTO `tb_sys_menu_permission` (id, permission_id, menu_id, creator, create_time) VALUES
|
INSERT INTO `tb_sys_menu_permission` (id, permission_id, menu_id, creator, create_time) VALUES
|
||||||
-- 前端菜单权限关联
|
-- 前端菜单权限关联
|
||||||
@@ -174,7 +189,6 @@ INSERT INTO `tb_sys_menu_permission` (id, permission_id, menu_id, creator, creat
|
|||||||
('115', 'perm_default', 'menu_profile', '1', now()),
|
('115', 'perm_default', 'menu_profile', '1', now()),
|
||||||
('116', 'perm_default', 'menu_personal_info', '1', now()),
|
('116', 'perm_default', 'menu_personal_info', '1', now()),
|
||||||
('117', 'perm_default', 'menu_account_settings', '1', now()),
|
('117', 'perm_default', 'menu_account_settings', '1', now()),
|
||||||
('118', 'perm_ai_manage', 'menu_ai_assistant', '1', now()),
|
|
||||||
('119', 'perm_default', 'menu_user_dropdown', '1', now()),
|
('119', 'perm_default', 'menu_user_dropdown', '1', now()),
|
||||||
('120', 'perm_news_article_add', 'menu_article_add', '1', now()),
|
('120', 'perm_news_article_add', 'menu_article_add', '1', now()),
|
||||||
('121', 'perm_default', 'menu_task_detail', '1', now()),
|
('121', 'perm_default', 'menu_task_detail', '1', now()),
|
||||||
@@ -220,4 +234,15 @@ INSERT INTO `tb_sys_menu_permission` (id, permission_id, menu_id, creator, creat
|
|||||||
('232', 'perm_crontab_manage', 'menu_admin_crontab_manage', '1', now()),
|
('232', 'perm_crontab_manage', 'menu_admin_crontab_manage', '1', now()),
|
||||||
('233', 'perm_crontab_manage', 'menu_admin_crontab_task', '1', now()),
|
('233', 'perm_crontab_manage', 'menu_admin_crontab_task', '1', now()),
|
||||||
('234', 'perm_crontab_manage', 'menu_admin_crontab_log', '1', now()),
|
('234', 'perm_crontab_manage', 'menu_admin_crontab_log', '1', now()),
|
||||||
('235', 'perm_crontab_manage', 'menu_admin_news_crawler', '1', now());
|
('235', 'perm_crontab_manage', 'menu_admin_news_crawler', '1', now()),
|
||||||
|
|
||||||
|
-- 消息通知管理菜单权限关联
|
||||||
|
('240', 'perm_message_manage', 'menu_admin_message_manage', '1', now()),
|
||||||
|
('241', 'perm_message_manage', 'menu_admin_message_list', '1', now()),
|
||||||
|
('242', 'perm_message_send', 'menu_admin_message_create', '1', now()),
|
||||||
|
('243', 'perm_message_manage', 'menu_admin_message_detail', '1', now()),
|
||||||
|
|
||||||
|
-- 用户端消息中心权限关联
|
||||||
|
('250', 'perm_message_view', 'menu_user_message_center', '1', now()),
|
||||||
|
('251', 'perm_message_view', 'menu_user_message_detail', '1', now());
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ org.xyzh.achievement.controller.AchievementController
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-usercenter</artifactId> <!-- 包含成就接口 -->
|
<artifactId>api-usercenter</artifactId> <!-- 包含成就接口 -->
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -87,14 +87,14 @@ org.xyzh.achievement.controller.AchievementController
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-achievement</artifactId>
|
<artifactId>api-achievement</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 如果需要使用实现类(通常不需要,使用Dubbo远程调用) -->
|
<!-- 如果需要使用实现类(通常不需要,使用Dubbo远程调用) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>achievement</artifactId>
|
<artifactId>achievement</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>achievement</artifactId>
|
<artifactId>achievement</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>achievement</name>
|
<name>achievement</name>
|
||||||
<description>成就模块</description>
|
<description>成就模块</description>
|
||||||
@@ -25,28 +25,28 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-achievement</artifactId>
|
<artifactId>api-achievement</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-system</artifactId>
|
<artifactId>api-system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-study</artifactId>
|
<artifactId>api-study</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-all</artifactId>
|
<artifactId>common-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>system</artifactId>
|
<artifactId>system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>admin</artifactId>
|
<artifactId>admin</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
@@ -34,57 +34,62 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>auth</artifactId>
|
<artifactId>auth</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-all</artifactId>
|
<artifactId>common-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-all</artifactId>
|
<artifactId>api-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>system</artifactId>
|
<artifactId>system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>news</artifactId>
|
<artifactId>news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>study</artifactId>
|
<artifactId>study</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>usercenter</artifactId>
|
<artifactId>usercenter</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>achievement</artifactId>
|
<artifactId>achievement</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>file</artifactId>
|
<artifactId>file</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>ai</artifactId>
|
<artifactId>ai</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>crontab</artifactId>
|
<artifactId>crontab</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>message</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
|||||||
@SpringBootApplication(scanBasePackages = "org.xyzh")
|
@SpringBootApplication(scanBasePackages = "org.xyzh")
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@MapperScan({"org.xyzh.system.mapper", "org.xyzh.file.mapper", "org.xyzh.news.mapper", "org.xyzh.study.mapper",
|
@MapperScan({"org.xyzh.system.mapper", "org.xyzh.file.mapper", "org.xyzh.news.mapper", "org.xyzh.study.mapper",
|
||||||
"org.xyzh.usercenter.mapper", "org.xyzh.ai.mapper", "org.xyzh.achievement.mapper", "org.xyzh.crontab.mapper"})
|
"org.xyzh.usercenter.mapper", "org.xyzh.ai.mapper", "org.xyzh.achievement.mapper", "org.xyzh.crontab.mapper",
|
||||||
|
"org.xyzh.message.mapper"})
|
||||||
public class App {
|
public class App {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -114,7 +114,9 @@
|
|||||||
<Logger name="org.xyzh.crontab.mapper" level="debug" additivity="false">
|
<Logger name="org.xyzh.crontab.mapper" level="debug" additivity="false">
|
||||||
<AppenderRef ref="Console"/>
|
<AppenderRef ref="Console"/>
|
||||||
</Logger>
|
</Logger>
|
||||||
|
<Logger name="org.xyzh.message.mapper" level="debug" additivity="false">
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
</Logger>
|
||||||
<!-- 项目包日志配置 - Auth模块 -->
|
<!-- 项目包日志配置 - Auth模块 -->
|
||||||
<Logger name="org.xyzh.auth" level="debug" additivity="false">
|
<Logger name="org.xyzh.auth" level="debug" additivity="false">
|
||||||
<AppenderRef ref="Console"/>
|
<AppenderRef ref="Console"/>
|
||||||
@@ -173,6 +175,14 @@
|
|||||||
<AppenderRef ref="RollingFileError"/>
|
<AppenderRef ref="RollingFileError"/>
|
||||||
<AppenderRef ref="DatabaseAppender"/>
|
<AppenderRef ref="DatabaseAppender"/>
|
||||||
</Logger>
|
</Logger>
|
||||||
|
<Logger name="org.xyzh.message" level="debug" additivity="false">
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
<AppenderRef ref="Filelog"/>
|
||||||
|
<AppenderRef ref="RollingFileInfo"/>
|
||||||
|
<AppenderRef ref="RollingFileWarn"/>
|
||||||
|
<AppenderRef ref="RollingFileError"/>
|
||||||
|
<AppenderRef ref="DatabaseAppender"/>
|
||||||
|
</Logger>
|
||||||
|
|
||||||
<root level="info">
|
<root level="info">
|
||||||
<appender-ref ref="Console"/>
|
<appender-ref ref="Console"/>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>ai</artifactId>
|
<artifactId>ai</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>ai</name>
|
<name>ai</name>
|
||||||
<description>智能体模块</description>
|
<description>智能体模块</description>
|
||||||
@@ -25,18 +25,18 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-ai</artifactId>
|
<artifactId>api-ai</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-file</artifactId>
|
<artifactId>api-file</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-all</artifactId>
|
<artifactId>common-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-achievement</artifactId>
|
<artifactId>api-achievement</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>api-achievement</name>
|
<name>api-achievement</name>
|
||||||
<description>成就模块API接口</description>
|
<description>成就模块API接口</description>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-ai</artifactId>
|
<artifactId>api-ai</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<name>api-ai</name>
|
<name>api-ai</name>
|
||||||
<description>智能体API接口定义</description>
|
<description>智能体API接口定义</description>
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-all</artifactId>
|
<artifactId>api-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
@@ -57,5 +57,9 @@
|
|||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-crontab</artifactId>
|
<artifactId>api-crontab</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>api-message</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-auth</artifactId>
|
<artifactId>api-auth</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-crontab</artifactId>
|
<artifactId>api-crontab</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
@@ -22,12 +22,12 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-core</artifactId>
|
<artifactId>common-core</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-dto</artifactId>
|
<artifactId>common-dto</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-file</artifactId>
|
<artifactId>api-file</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>api-file</name>
|
<name>api-file</name>
|
||||||
<description>文件模块API</description>
|
<description>文件模块API</description>
|
||||||
|
|||||||
33
schoolNewsServ/api/api-message/pom.xml
Normal file
33
schoolNewsServ/api/api-message/pom.xml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>api</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>api-message</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>common-core</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>common-dto</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package org.xyzh.api.message;
|
||||||
|
|
||||||
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
|
import org.xyzh.common.core.page.PageParam;
|
||||||
|
import org.xyzh.common.dto.message.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息服务接口
|
||||||
|
*
|
||||||
|
* @description 消息通知模块的服务接口定义
|
||||||
|
* @filename MessageService.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
public interface MessageService {
|
||||||
|
|
||||||
|
// ================== 消息管理 ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建消息
|
||||||
|
*
|
||||||
|
* @param message 消息对象
|
||||||
|
* @return ResultDomain<TbSysMessage> 创建结果
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<TbSysMessage> createMessage(TbSysMessage message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新消息(仅允许更新草稿状态的消息)
|
||||||
|
*
|
||||||
|
* @param message 消息对象
|
||||||
|
* @return ResultDomain<TbSysMessage> 更新结果
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<TbSysMessage> updateMessage(TbSysMessage message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消息(逻辑删除)
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessage> 删除结果
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<TbSysMessage> deleteMessage(String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询消息详情
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<MessageVO> 消息详情
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<MessageVO> getMessageById(String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询消息列表(管理端)
|
||||||
|
*
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @param pageParam 分页参数
|
||||||
|
* @return ResultDomain<MessageVO> 消息列表
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<MessageVO> getMessagePage(TbSysMessage filter, PageParam pageParam);
|
||||||
|
|
||||||
|
// ================== 消息发送 ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息(立即发送或定时发送)
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessage> 发送结果
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<TbSysMessage> sendMessage(String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立即发送(将定时消息改为立即发送)
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessage> 发送结果
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<TbSysMessage> sendNow(String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消定时消息
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessage> 取消结果
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<TbSysMessage> cancelMessage(String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改定时发送时间
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @param scheduledTime 新的发送时间
|
||||||
|
* @return ResultDomain<TbSysMessage> 修改结果
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<TbSysMessage> rescheduleMessage(String messageID, java.util.Date scheduledTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试失败消息
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessage> 重试结果
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<TbSysMessage> retryMessage(String messageID);
|
||||||
|
|
||||||
|
// ================== 用户消息 ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询我的消息列表(用户端)
|
||||||
|
*
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @param pageParam 分页参数
|
||||||
|
* @return ResultDomain<MessageUserVO> 我的消息列表
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<MessageUserVO> getMyMessagesPage(MessageUserVO filter, PageParam pageParam);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询我的消息详情(用户端)
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<MessageUserVO> 消息详情
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<MessageUserVO> getMyMessageDetail(String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记消息为已读
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessageUser> 标记结果
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<TbSysMessageUser> markAsRead(String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量标记消息为已读
|
||||||
|
*
|
||||||
|
* @param messageIDs 消息ID列表
|
||||||
|
* @return ResultDomain<Integer> 标记成功的数量
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<Integer> batchMarkAsRead(List<String> messageIDs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取未读消息数量
|
||||||
|
*
|
||||||
|
* @return ResultDomain<Integer> 未读消息数量
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<Integer> getUnreadCount();
|
||||||
|
|
||||||
|
// ================== 辅助接口 ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可选的部门树(当前部门及子部门)
|
||||||
|
*
|
||||||
|
* @return ResultDomain<Map> 部门树数据
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<Map<String, Object>> getTargetDepts();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可选的角色列表(当前部门及子部门的角色)
|
||||||
|
*
|
||||||
|
* @return ResultDomain<Map> 角色列表数据
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<Map<String, Object>> getTargetRoles();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可选的用户列表(当前部门及子部门的用户)
|
||||||
|
*
|
||||||
|
* @return ResultDomain<Map> 用户列表数据
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
ResultDomain<Map<String, Object>> getTargetUsers();
|
||||||
|
}
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-news</artifactId>
|
<artifactId>api-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>api-news</name>
|
<name>api-news</name>
|
||||||
<description>新闻API接口定义</description>
|
<description>新闻API接口定义</description>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-study</artifactId>
|
<artifactId>api-study</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-system</artifactId>
|
<artifactId>api-system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-usercenter</artifactId>
|
<artifactId>api-usercenter</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<name>api-usercenter</name>
|
<name>api-usercenter</name>
|
||||||
<description>个人中心API接口定义</description>
|
<description>个人中心API接口定义</description>
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<modules>
|
<modules>
|
||||||
<module>api-all</module>
|
<module>api-all</module>
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
<module>api-news</module>
|
<module>api-news</module>
|
||||||
<module>api-file</module>
|
<module>api-file</module>
|
||||||
<module>api-crontab</module>
|
<module>api-crontab</module>
|
||||||
|
<module>api-message</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
@@ -36,57 +37,62 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-all</artifactId>
|
<artifactId>api-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-auth</artifactId>
|
<artifactId>api-auth</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-system</artifactId>
|
<artifactId>api-system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-course</artifactId>
|
<artifactId>api-course</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-study</artifactId>
|
<artifactId>api-study</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-usercenter</artifactId>
|
<artifactId>api-usercenter</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-achievement</artifactId>
|
<artifactId>api-achievement</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-ai</artifactId>
|
<artifactId>api-ai</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-news</artifactId>
|
<artifactId>api-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-file</artifactId>
|
<artifactId>api-file</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-crontab</artifactId>
|
<artifactId>api-crontab</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>api-message</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
@@ -94,12 +100,12 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-core</artifactId>
|
<artifactId>common-core</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-dto</artifactId>
|
<artifactId>common-dto</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>auth</artifactId>
|
<artifactId>auth</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
@@ -24,38 +24,38 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-core</artifactId>
|
<artifactId>common-core</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-dto</artifactId>
|
<artifactId>common-dto</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-exception</artifactId>
|
<artifactId>common-exception</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-redis</artifactId>
|
<artifactId>common-redis</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-util</artifactId>
|
<artifactId>common-util</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-auth</artifactId>
|
<artifactId>api-auth</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-system</artifactId>
|
<artifactId>api-system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common</artifactId>
|
<artifactId>common</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-all</artifactId>
|
<artifactId>common-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>Common All-in-One</name>
|
<name>Common All-in-One</name>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common</artifactId>
|
<artifactId>common</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-annotation</artifactId>
|
<artifactId>common-annotation</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common</artifactId>
|
<artifactId>common</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-core</artifactId>
|
<artifactId>common-core</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
|||||||
@@ -6,16 +6,22 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common</artifactId>
|
<artifactId>common</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-dto</artifactId>
|
<artifactId>common-dto</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
</properties>
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package org.xyzh.common.dto.message;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建消息请求DTO
|
||||||
|
*
|
||||||
|
* @description 用于接收前端创建消息的请求参数
|
||||||
|
* @filename MessageCreateDTO.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class MessageCreateDTO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息标题(必填)
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息内容(必填)
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型:notification-通知/announcement-公告/warning-预警
|
||||||
|
* 默认:notification
|
||||||
|
*/
|
||||||
|
private String messageType = "notification";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先级:normal-普通/important-重要/urgent-紧急
|
||||||
|
* 默认:normal
|
||||||
|
*/
|
||||||
|
private String priority = "normal";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送模式:immediate-立即发送/scheduled-定时发送
|
||||||
|
* 默认:immediate
|
||||||
|
*/
|
||||||
|
private String sendMode = "immediate";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计划发送时间(sendMode=scheduled时必填)
|
||||||
|
*/
|
||||||
|
private Date scheduledTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收对象列表(必填)
|
||||||
|
*/
|
||||||
|
private List<MessageTargetDTO> targets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部类:接收对象DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class MessageTargetDTO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送方式:system/email/sms,多选时逗号分隔
|
||||||
|
* 默认:system
|
||||||
|
*/
|
||||||
|
private String sendMethod = "system";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收对象类型:dept-部门/role-角色/user-人员
|
||||||
|
*/
|
||||||
|
private String targetType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收对象ID
|
||||||
|
*/
|
||||||
|
private String targetID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收对象名称(前端传递,用于冗余存储)
|
||||||
|
*/
|
||||||
|
private String targetName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作用域部门ID(关键字段)
|
||||||
|
* - dept时:与targetID相同
|
||||||
|
* - role时:限定该角色的部门范围
|
||||||
|
* - user时:用户所属部门ID
|
||||||
|
*/
|
||||||
|
private String scopeDeptID;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package org.xyzh.common.dto.message;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户消息视图对象
|
||||||
|
*
|
||||||
|
* @description 用于前端展示的用户消息对象,包含用户信息和消息阅读状态
|
||||||
|
* @filename MessageUserVO.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class MessageUserVO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息ID
|
||||||
|
*/
|
||||||
|
private String messageID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private String userID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户姓名
|
||||||
|
*/
|
||||||
|
private String fullName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户所属部门ID
|
||||||
|
*/
|
||||||
|
private String deptID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户所属部门名称
|
||||||
|
*/
|
||||||
|
private String deptName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实际发送方式:system-系统消息/email-邮件/sms-短信
|
||||||
|
*/
|
||||||
|
private String sendMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已读:0-未读,1-已读
|
||||||
|
*/
|
||||||
|
private Boolean isRead;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阅读时间
|
||||||
|
*/
|
||||||
|
private Date readTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送状态:pending-待发送/success-发送成功/failed-发送失败
|
||||||
|
*/
|
||||||
|
private String sendStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败原因
|
||||||
|
*/
|
||||||
|
private String failReason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
// ========== 消息主体信息(用户端查看消息列表时需要) ==========
|
||||||
|
/**
|
||||||
|
* 消息标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息内容
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型
|
||||||
|
*/
|
||||||
|
private String messageType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先级
|
||||||
|
*/
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送人姓名
|
||||||
|
*/
|
||||||
|
private String senderName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送人部门名称
|
||||||
|
*/
|
||||||
|
private String senderDeptName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实际发送时间
|
||||||
|
*/
|
||||||
|
private Date actualSendTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package org.xyzh.common.dto.message;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息视图对象
|
||||||
|
*
|
||||||
|
* @description 用于前端展示的消息对象,包含消息主体信息和关联的接收对象列表
|
||||||
|
* @filename MessageVO.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class MessageVO implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
// ========== 消息主体信息 ==========
|
||||||
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息唯一标识
|
||||||
|
*/
|
||||||
|
private String messageID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息内容
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型:notification-通知/announcement-公告/warning-预警
|
||||||
|
*/
|
||||||
|
private String messageType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先级:normal-普通/important-重要/urgent-紧急
|
||||||
|
*/
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送人用户ID
|
||||||
|
*/
|
||||||
|
private String senderID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送人姓名
|
||||||
|
*/
|
||||||
|
private String senderName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送人部门ID
|
||||||
|
*/
|
||||||
|
private String senderDeptID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送人部门名称
|
||||||
|
*/
|
||||||
|
private String senderDeptName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送模式:immediate-立即发送/scheduled-定时发送
|
||||||
|
*/
|
||||||
|
private String sendMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计划发送时间
|
||||||
|
*/
|
||||||
|
private Date scheduledTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实际发送时间
|
||||||
|
*/
|
||||||
|
private Date actualSendTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:draft-草稿/pending-待发送/sending-发送中/sent-已发送/failed-失败/cancelled-已取消
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标用户总数
|
||||||
|
*/
|
||||||
|
private Integer targetUserCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已发送数量
|
||||||
|
*/
|
||||||
|
private Integer sentCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送成功数量
|
||||||
|
*/
|
||||||
|
private Integer successCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送失败数量
|
||||||
|
*/
|
||||||
|
private Integer failedCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已读数量
|
||||||
|
*/
|
||||||
|
private Integer readCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前重试次数
|
||||||
|
*/
|
||||||
|
private Integer retryCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大重试次数
|
||||||
|
*/
|
||||||
|
private Integer maxRetryCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后错误信息
|
||||||
|
*/
|
||||||
|
private String lastError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
|
// ========== 关联信息 ==========
|
||||||
|
/**
|
||||||
|
* 接收对象列表
|
||||||
|
*/
|
||||||
|
private List<TbSysMessageTarget> targets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户接收记录列表(仅在详情查询时返回)
|
||||||
|
*/
|
||||||
|
private List<MessageUserVO> userMessages;
|
||||||
|
|
||||||
|
// ========== 计算属性 ==========
|
||||||
|
/**
|
||||||
|
* 发送进度百分比(0-100)
|
||||||
|
*/
|
||||||
|
public Integer getSendProgress() {
|
||||||
|
if (targetUserCount == null || targetUserCount == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (sentCount == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int) Math.round((sentCount * 100.0) / targetUserCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功率百分比(0-100)
|
||||||
|
*/
|
||||||
|
public Integer getSuccessRate() {
|
||||||
|
if (sentCount == null || sentCount == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (successCount == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int) Math.round((successCount * 100.0) / sentCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已读率百分比(0-100)
|
||||||
|
*/
|
||||||
|
public Integer getReadRate() {
|
||||||
|
if (successCount == null || successCount == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (readCount == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int) Math.round((readCount * 100.0) / successCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package org.xyzh.common.dto.message;
|
||||||
|
|
||||||
|
import org.xyzh.common.dto.BaseDTO;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息主体表实体类
|
||||||
|
*
|
||||||
|
* @description 消息通知模块的主体表,存储消息的基本信息、发送配置和统计数据
|
||||||
|
* @filename TbSysMessage.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class TbSysMessage extends BaseDTO {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息唯一标识
|
||||||
|
*/
|
||||||
|
private String messageID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息内容
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型:notification-通知/announcement-公告/warning-预警
|
||||||
|
*/
|
||||||
|
private String messageType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先级:normal-普通/important-重要/urgent-紧急
|
||||||
|
*/
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送人用户ID
|
||||||
|
*/
|
||||||
|
private String senderID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送人姓名(冗余字段)
|
||||||
|
*/
|
||||||
|
private String senderName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送人部门ID
|
||||||
|
*/
|
||||||
|
private String senderDeptID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送人部门名称(冗余字段)
|
||||||
|
*/
|
||||||
|
private String senderDeptName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送模式:immediate-立即发送/scheduled-定时发送
|
||||||
|
*/
|
||||||
|
private String sendMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计划发送时间(sendMode=scheduled时必填)
|
||||||
|
*/
|
||||||
|
private Date scheduledTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实际发送时间
|
||||||
|
*/
|
||||||
|
private Date actualSendTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态:draft-草稿/pending-待发送/sending-发送中/sent-已发送/failed-失败/cancelled-已取消
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标用户总数
|
||||||
|
*/
|
||||||
|
private Integer targetUserCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已发送数量
|
||||||
|
*/
|
||||||
|
private Integer sentCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送成功数量
|
||||||
|
*/
|
||||||
|
private Integer successCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送失败数量
|
||||||
|
*/
|
||||||
|
private Integer failedCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已读数量
|
||||||
|
*/
|
||||||
|
private Integer readCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前重试次数
|
||||||
|
*/
|
||||||
|
private Integer retryCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大重试次数
|
||||||
|
*/
|
||||||
|
private Integer maxRetryCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最后错误信息
|
||||||
|
*/
|
||||||
|
private String lastError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建人ID
|
||||||
|
*/
|
||||||
|
private String creator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新人ID
|
||||||
|
*/
|
||||||
|
private String updater;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送目标配置列表(前端辅助字段,不映射到数据库)
|
||||||
|
*/
|
||||||
|
private List<TbSysMessageTarget> targets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送方式列表(前端辅助字段,从targets聚合)
|
||||||
|
*/
|
||||||
|
private List<String> sendMethods;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package org.xyzh.common.dto.message;
|
||||||
|
|
||||||
|
import org.xyzh.common.dto.BaseDTO;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息发送方式接收对象表实体类
|
||||||
|
*
|
||||||
|
* @description 存储消息的接收对象配置,包括发送方式、目标类型和作用域部门
|
||||||
|
* @filename TbSysMessageTarget.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class TbSysMessageTarget extends BaseDTO {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息ID(关联tb_sys_message.messageID)
|
||||||
|
*/
|
||||||
|
private String messageID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送方式:system-系统消息/email-邮件/sms-短信
|
||||||
|
* 多选时逗号分隔,如:system,email
|
||||||
|
*/
|
||||||
|
private String sendMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收对象类型:dept-部门/role-角色/user-人员
|
||||||
|
*/
|
||||||
|
private String targetType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收对象ID(部门ID/角色ID/用户ID)
|
||||||
|
*/
|
||||||
|
private String targetID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收对象名称(冗余字段,便于展示)
|
||||||
|
*/
|
||||||
|
private String targetName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 作用域部门ID
|
||||||
|
* - dept时:与targetID相同
|
||||||
|
* - role时:表示该角色限定在哪个部门及其子部门范围内
|
||||||
|
* - user时:用户所属部门ID
|
||||||
|
*/
|
||||||
|
private String scopeDeptID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建人ID
|
||||||
|
*/
|
||||||
|
private String creator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新人ID
|
||||||
|
*/
|
||||||
|
private String updater;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package org.xyzh.common.dto.message;
|
||||||
|
|
||||||
|
import org.xyzh.common.dto.BaseDTO;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户接收消息表实体类
|
||||||
|
*
|
||||||
|
* @description 存储每个用户接收到的消息记录,包括阅读状态和发送状态
|
||||||
|
* @filename TbSysMessageUser.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class TbSysMessageUser extends BaseDTO {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息ID(关联tb_sys_message.messageID)
|
||||||
|
*/
|
||||||
|
private String messageID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收用户ID
|
||||||
|
*/
|
||||||
|
private String userID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实际发送方式:system-系统消息/email-邮件/sms-短信
|
||||||
|
*/
|
||||||
|
private String sendMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已读:0-未读,1-已读
|
||||||
|
*/
|
||||||
|
private Boolean isRead;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阅读时间
|
||||||
|
*/
|
||||||
|
private Date readTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送状态:pending-待发送/success-发送成功/failed-发送失败
|
||||||
|
*/
|
||||||
|
private String sendStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败原因
|
||||||
|
*/
|
||||||
|
private String failReason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建人ID
|
||||||
|
*/
|
||||||
|
private String creator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新人ID
|
||||||
|
*/
|
||||||
|
private String updater;
|
||||||
|
}
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common</artifactId>
|
<artifactId>common</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-exception</artifactId>
|
<artifactId>common-exception</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package org.xyzh;
|
|
||||||
|
|
||||||
public class Main {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
System.out.println("Hello world!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common</artifactId>
|
<artifactId>common</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-jdbc</artifactId>
|
<artifactId>common-jdbc</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-dto</artifactId>
|
<artifactId>common-dto</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common</artifactId>
|
<artifactId>common</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-redis</artifactId>
|
<artifactId>common-redis</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common</artifactId>
|
<artifactId>common</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-util</artifactId>
|
<artifactId>common-util</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common</artifactId>
|
<artifactId>common</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<modules>
|
<modules>
|
||||||
<module>common-core</module>
|
<module>common-core</module>
|
||||||
@@ -42,37 +42,37 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-core</artifactId>
|
<artifactId>common-core</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-dto</artifactId>
|
<artifactId>common-dto</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-annotation</artifactId>
|
<artifactId>common-annotation</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-exception</artifactId>
|
<artifactId>common-exception</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-redis</artifactId>
|
<artifactId>common-redis</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-jdbc</artifactId>
|
<artifactId>common-jdbc</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-util</artifactId>
|
<artifactId>common-util</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>crontab</artifactId>
|
<artifactId>crontab</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
@@ -23,25 +23,25 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-crontab</artifactId>
|
<artifactId>api-crontab</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-news</artifactId>
|
<artifactId>api-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Common模块依赖 -->
|
<!-- Common模块依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-all</artifactId>
|
<artifactId>common-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- System模块依赖 -->
|
<!-- System模块依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>system</artifactId>
|
<artifactId>system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>file</artifactId>
|
<artifactId>file</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>file</name>
|
<name>file</name>
|
||||||
<description>文件模块</description>
|
<description>文件模块</description>
|
||||||
@@ -27,14 +27,14 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-file</artifactId>
|
<artifactId>api-file</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 通用模块 -->
|
<!-- 通用模块 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-all</artifactId>
|
<artifactId>common-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Spring Boot Web -->
|
<!-- Spring Boot Web -->
|
||||||
|
|||||||
96
schoolNewsServ/message/pom.xml
Normal file
96
schoolNewsServ/message/pom.xml
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>school-news</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>message</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- API依赖 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>api-message</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>api-system</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Common模块依赖 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>common-all</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- System模块依赖(需要部门、角色、用户相关服务)-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>system</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-logging</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.zaxxer</groupId>
|
||||||
|
<artifactId>HikariCP</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-log4j2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<parameters>true</parameters>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package org.xyzh.message.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.TaskScheduler;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.annotation.SchedulingConfigurer;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
|
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时任务和异步任务配置类
|
||||||
|
*
|
||||||
|
* @description 配置消息模块的定时任务线程池和异步任务线程池
|
||||||
|
* @filename SchedulingConfig.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
public class SchedulingConfig implements SchedulingConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
|
||||||
|
taskRegistrar.setScheduler(messageSchedulerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "messageSchedulerExecutor")
|
||||||
|
public TaskScheduler messageSchedulerExecutor() {
|
||||||
|
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
|
||||||
|
// 线程池大小:5个线程用于定时任务扫描
|
||||||
|
executor.setPoolSize(5);
|
||||||
|
// 线程名称前缀
|
||||||
|
executor.setThreadNamePrefix("message-scheduler-");
|
||||||
|
// 等待所有任务完成后再关闭
|
||||||
|
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||||
|
// 等待时间(秒)
|
||||||
|
executor.setAwaitTerminationSeconds(60);
|
||||||
|
// 初始化
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步任务线程池配置
|
||||||
|
*/
|
||||||
|
@Bean(name = "messageAsyncExecutor")
|
||||||
|
public Executor messageAsyncExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
// 核心线程数:10个线程用于异步发送消息
|
||||||
|
executor.setCorePoolSize(10);
|
||||||
|
// 最大线程数
|
||||||
|
executor.setMaxPoolSize(20);
|
||||||
|
// 队列容量
|
||||||
|
executor.setQueueCapacity(500);
|
||||||
|
// 线程名称前缀
|
||||||
|
executor.setThreadNamePrefix("message-async-");
|
||||||
|
// 等待所有任务完成后再关闭
|
||||||
|
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||||
|
// 等待时间(秒)
|
||||||
|
executor.setAwaitTerminationSeconds(60);
|
||||||
|
// 初始化
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
package org.xyzh.message.controller;
|
||||||
|
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.xyzh.api.message.MessageService;
|
||||||
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
|
import org.xyzh.common.core.page.PageParam;
|
||||||
|
import org.xyzh.common.core.page.PageRequest;
|
||||||
|
import org.xyzh.common.dto.message.*;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息控制器
|
||||||
|
*
|
||||||
|
* @description 消息通知模块的REST API接口
|
||||||
|
* @filename MessageController.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/message")
|
||||||
|
public class MessageController {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MessageController.class);
|
||||||
|
@Autowired
|
||||||
|
private MessageService messageService;
|
||||||
|
|
||||||
|
// ================== 消息管理接口(管理端) ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建消息
|
||||||
|
*
|
||||||
|
* @param message 消息对象
|
||||||
|
* @return ResultDomain<TbSysMessage>
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
public ResultDomain<TbSysMessage> createMessage(@RequestBody TbSysMessage message) {
|
||||||
|
logger.info("创建消息:{}", message.getTitle());
|
||||||
|
return messageService.createMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新消息(仅草稿状态)
|
||||||
|
*
|
||||||
|
* @param message 消息对象
|
||||||
|
* @return ResultDomain<TbSysMessage>
|
||||||
|
*/
|
||||||
|
@PutMapping
|
||||||
|
public ResultDomain<TbSysMessage> updateMessage(@RequestBody TbSysMessage message) {
|
||||||
|
logger.info("更新消息:{}", message.getMessageID());
|
||||||
|
return messageService.updateMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消息
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessage>
|
||||||
|
*/
|
||||||
|
@DeleteMapping
|
||||||
|
public ResultDomain<TbSysMessage> deleteMessage(@RequestBody TbSysMessage message) {
|
||||||
|
return messageService.deleteMessage(message.getMessageID());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询消息详情
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<MessageVO>
|
||||||
|
*/
|
||||||
|
@GetMapping("/detail/{messageID}")
|
||||||
|
public ResultDomain<MessageVO> getMessageById(@PathVariable String messageID) {
|
||||||
|
return messageService.getMessageById(messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询消息列表(管理端)
|
||||||
|
*
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @param pageParam 分页参数
|
||||||
|
* @return ResultDomain<MessageVO>
|
||||||
|
*/
|
||||||
|
@PostMapping("/page")
|
||||||
|
public ResultDomain<MessageVO> getMessagePage(@RequestBody TbSysMessage filter, PageParam pageParam) {
|
||||||
|
return messageService.getMessagePage(filter, pageParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ================== 消息发送接口 ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessage>
|
||||||
|
*/
|
||||||
|
@PostMapping("/send/{messageID}")
|
||||||
|
public ResultDomain<TbSysMessage> sendMessage(@PathVariable String messageID) {
|
||||||
|
logger.info("发送消息:{}", messageID);
|
||||||
|
return messageService.sendMessage(messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立即发送(定时消息改为立即发送)
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessage>
|
||||||
|
*/
|
||||||
|
@PostMapping("/sendNow/{messageID}")
|
||||||
|
public ResultDomain<TbSysMessage> sendNow(@PathVariable String messageID) {
|
||||||
|
logger.info("立即发送消息:{}", messageID);
|
||||||
|
return messageService.sendNow(messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消定时消息
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessage>
|
||||||
|
*/
|
||||||
|
@PostMapping("/cancel/{messageID}")
|
||||||
|
public ResultDomain<TbSysMessage> cancelMessage(@PathVariable String messageID) {
|
||||||
|
logger.info("取消消息:{}", messageID);
|
||||||
|
return messageService.cancelMessage(messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改定时发送时间
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @param request 包含scheduledTime的请求对象
|
||||||
|
* @return ResultDomain<TbSysMessage>
|
||||||
|
*/
|
||||||
|
@PutMapping("/reschedule/{messageID}")
|
||||||
|
public ResultDomain<TbSysMessage> rescheduleMessage(@PathVariable String messageID,
|
||||||
|
@RequestBody Map<String, Object> request) {
|
||||||
|
logger.info("修改消息发送时间:{}", messageID);
|
||||||
|
Date scheduledTime = (Date) request.get("scheduledTime");
|
||||||
|
return messageService.rescheduleMessage(messageID, scheduledTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试失败消息
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessage>
|
||||||
|
*/
|
||||||
|
@PostMapping("/retry/{messageID}")
|
||||||
|
public ResultDomain<TbSysMessage> retryMessage(@PathVariable String messageID) {
|
||||||
|
logger.info("重试消息:{}", messageID);
|
||||||
|
return messageService.retryMessage(messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================== 用户消息接口(用户端) ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询我的消息列表
|
||||||
|
*
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @param pageParam 分页参数
|
||||||
|
* @return ResultDomain<MessageUserVO>
|
||||||
|
*/
|
||||||
|
@PostMapping("/my/page")
|
||||||
|
public ResultDomain<MessageUserVO> getMyMessagesPage(@RequestBody PageRequest<MessageUserVO> pageRequest) {
|
||||||
|
return messageService.getMyMessagesPage(pageRequest.getFilter(), pageRequest.getPageParam());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询我的消息详情
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<MessageUserVO>
|
||||||
|
*/
|
||||||
|
@GetMapping("/my/detail/{messageID}")
|
||||||
|
public ResultDomain<MessageUserVO> getMyMessageDetail(@PathVariable String messageID) {
|
||||||
|
return messageService.getMyMessageDetail(messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记消息为已读
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return ResultDomain<TbSysMessageUser>
|
||||||
|
*/
|
||||||
|
@PostMapping("/my/markRead/{messageID}")
|
||||||
|
public ResultDomain<TbSysMessageUser> markAsRead(@PathVariable String messageID) {
|
||||||
|
return messageService.markAsRead(messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量标记消息为已读
|
||||||
|
*
|
||||||
|
* @param request 包含messageIDs的请求对象
|
||||||
|
* @return ResultDomain<Integer>
|
||||||
|
*/
|
||||||
|
@PostMapping("/my/batchMarkRead")
|
||||||
|
public ResultDomain<Integer> batchMarkAsRead(@RequestBody Map<String, List<String>> request) {
|
||||||
|
List<String> messageIDs = request.get("messageIDs");
|
||||||
|
return messageService.batchMarkAsRead(messageIDs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取未读消息数量
|
||||||
|
*
|
||||||
|
* @return ResultDomain<Integer>
|
||||||
|
*/
|
||||||
|
@GetMapping("/my/unreadCount")
|
||||||
|
public ResultDomain<Integer> getUnreadCount() {
|
||||||
|
return messageService.getUnreadCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================== 辅助接口 ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可选的部门树
|
||||||
|
*
|
||||||
|
* @return ResultDomain<Map>
|
||||||
|
*/
|
||||||
|
@GetMapping("/targets/depts")
|
||||||
|
public ResultDomain<Map<String, Object>> getTargetDepts() {
|
||||||
|
return messageService.getTargetDepts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可选的角色列表
|
||||||
|
*
|
||||||
|
* @return ResultDomain<Map>
|
||||||
|
*/
|
||||||
|
@GetMapping("/targets/roles")
|
||||||
|
public ResultDomain<Map<String, Object>> getTargetRoles() {
|
||||||
|
return messageService.getTargetRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可选的用户列表
|
||||||
|
*
|
||||||
|
* @return ResultDomain<Map>
|
||||||
|
*/
|
||||||
|
@GetMapping("/targets/users")
|
||||||
|
public ResultDomain<Map<String, Object>> getTargetUsers() {
|
||||||
|
return messageService.getTargetUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package org.xyzh.message.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.message.TbSysMessage;
|
||||||
|
import org.xyzh.common.dto.message.MessageVO;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息Mapper接口
|
||||||
|
*
|
||||||
|
* @description 消息主体表的数据访问接口
|
||||||
|
* @filename MessageMapper.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface MessageMapper extends BaseMapper<TbSysMessage> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据消息ID查询消息
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return TbSysMessage 消息信息
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
TbSysMessage selectMessageById(@Param("messageID") String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入消息
|
||||||
|
*
|
||||||
|
* @param message 消息信息
|
||||||
|
* @return int 影响行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int insertMessage(@Param("message") TbSysMessage message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新消息
|
||||||
|
*
|
||||||
|
* @param message 消息信息
|
||||||
|
* @return int 影响行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int updateMessage(@Param("message") TbSysMessage message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消息(逻辑删除)
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return int 影响行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int deleteMessage(@Param("messageID") String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计消息总数(带权限过滤)
|
||||||
|
*
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @param currentUserDeptID 当前用户部门ID
|
||||||
|
* @return int 总数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int countMessage(@Param("filter") TbSysMessage filter,
|
||||||
|
@Param("currentUserDeptID") String currentUserDeptID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询消息列表(带权限过滤)
|
||||||
|
*
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @param currentUserDeptID 当前用户部门ID
|
||||||
|
* @return List<MessageVO> 消息列表
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
List<MessageVO> selectMessagePage(@Param("filter") TbSysMessage filter,
|
||||||
|
@Param("currentUserDeptID") String currentUserDeptID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询消息详情
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return MessageVO 消息详情
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
MessageVO selectMessageDetail(@Param("messageID") String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询待发送的定时消息
|
||||||
|
*
|
||||||
|
* @param currentTime 当前时间
|
||||||
|
* @return List<TbSysMessage> 待发送的消息列表
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
List<TbSysMessage> selectPendingScheduledMessages(@Param("currentTime") LocalDateTime currentTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CAS更新消息状态(防止并发)
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @param expectedStatus 期望的当前状态
|
||||||
|
* @param newStatus 新状态
|
||||||
|
* @return int 更新的行数(1表示成功,0表示失败)
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int compareAndSetStatus(@Param("messageID") String messageID,
|
||||||
|
@Param("expectedStatus") String expectedStatus,
|
||||||
|
@Param("newStatus") String newStatus);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新消息统计信息
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @param sentCount 已发送数量
|
||||||
|
* @param successCount 成功数量
|
||||||
|
* @param failedCount 失败数量
|
||||||
|
* @return int 更新的行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int updateStatistics(@Param("messageID") String messageID,
|
||||||
|
@Param("sentCount") Integer sentCount,
|
||||||
|
@Param("successCount") Integer successCount,
|
||||||
|
@Param("failedCount") Integer failedCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新已读数量
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return int 更新的行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int incrementReadCount(@Param("messageID") String messageID);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.xyzh.message.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.message.TbSysMessageTarget;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息接收对象Mapper接口
|
||||||
|
*
|
||||||
|
* @description 消息接收对象表的数据访问接口
|
||||||
|
* @filename MessageTargetMapper.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface MessageTargetMapper extends BaseMapper<TbSysMessageTarget> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据消息ID查询接收对象列表
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return List<TbSysMessageTarget> 接收对象列表
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
List<TbSysMessageTarget> selectByMessageID(@Param("messageID") String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量插入接收对象
|
||||||
|
*
|
||||||
|
* @param targets 接收对象列表
|
||||||
|
* @return int 插入的行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int batchInsert(@Param("targets") List<TbSysMessageTarget> targets);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据消息ID删除接收对象
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return int 删除的行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int deleteByMessageID(@Param("messageID") String messageID);
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package org.xyzh.message.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.message.TbSysMessageUser;
|
||||||
|
import org.xyzh.common.dto.message.MessageUserVO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户消息Mapper接口
|
||||||
|
*
|
||||||
|
* @description 用户接收消息表的数据访问接口
|
||||||
|
* @filename MessageUserMapper.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface MessageUserMapper extends BaseMapper<TbSysMessageUser> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据消息ID查询用户消息列表
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return List<MessageUserVO> 用户消息列表
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
List<MessageUserVO> selectByMessageID(@Param("messageID") String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量插入用户消息
|
||||||
|
*
|
||||||
|
* @param userMessages 用户消息列表
|
||||||
|
* @return int 插入的行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int batchInsert(@Param("userMessages") List<TbSysMessageUser> userMessages);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询当前用户的消息列表
|
||||||
|
*
|
||||||
|
* @param userID 用户ID
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @return List<MessageUserVO> 消息列表
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
List<MessageUserVO> selectMyMessages(@Param("userID") String userID,
|
||||||
|
@Param("filter") TbSysMessageUser filter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的消息详情
|
||||||
|
*
|
||||||
|
* @param userID 用户ID
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return MessageUserVO 消息详情
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
MessageUserVO selectMyMessageDetail(@Param("userID") String userID,
|
||||||
|
@Param("messageID") String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记消息为已读
|
||||||
|
*
|
||||||
|
* @param userID 用户ID
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return int 更新的行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int markAsRead(@Param("userID") String userID,
|
||||||
|
@Param("messageID") String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量标记消息为已读
|
||||||
|
*
|
||||||
|
* @param userID 用户ID
|
||||||
|
* @param messageIDs 消息ID列表
|
||||||
|
* @return int 更新的行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int batchMarkAsRead(@Param("userID") String userID,
|
||||||
|
@Param("messageIDs") List<String> messageIDs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询未读消息数量
|
||||||
|
*
|
||||||
|
* @param userID 用户ID
|
||||||
|
* @return Integer 未读消息数量
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
Integer countUnread(@Param("userID") String userID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态计算未读消息数量(基于 target 配置)
|
||||||
|
*
|
||||||
|
* @param userID 用户ID
|
||||||
|
* @return Integer 未读消息数量
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
Integer countUnreadWithDynamicTargets(@Param("userID") String userID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户消息的发送状态
|
||||||
|
*
|
||||||
|
* @param id 用户消息ID
|
||||||
|
* @param sendStatus 发送状态
|
||||||
|
* @param failReason 失败原因(可选)
|
||||||
|
* @return int 更新的行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int updateSendStatus(@Param("id") String id,
|
||||||
|
@Param("sendStatus") String sendStatus,
|
||||||
|
@Param("failReason") String failReason);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询待发送的用户消息列表
|
||||||
|
*
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @return List<TbSysMessageUser> 用户消息列表
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
List<TbSysMessageUser> selectPendingUserMessages(@Param("filter") TbSysMessageUser filter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态查询当前用户可见的消息列表(基于 target 配置计算)
|
||||||
|
*
|
||||||
|
* @param userID 用户ID
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @return List<MessageUserVO> 消息列表
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
List<MessageUserVO> selectMyMessagesWithDynamicTargets(@Param("userID") String userID,
|
||||||
|
@Param("filter") MessageUserVO filter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询或创建用户消息记录(upsert)
|
||||||
|
*
|
||||||
|
* @param userID 用户ID
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @return MessageUserVO 用户消息记录
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
MessageUserVO selectOrCreateUserMessage(@Param("userID") String userID,
|
||||||
|
@Param("messageID") String messageID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入用户消息记录(如果不存在)
|
||||||
|
*
|
||||||
|
* @param userMessage 用户消息
|
||||||
|
* @return int 插入的行数
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
int insertIfNotExists(@Param("userMessage") TbSysMessageUser userMessage);
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
package org.xyzh.message.scheduler;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.xyzh.common.dto.message.TbSysMessage;
|
||||||
|
import org.xyzh.message.mapper.MessageMapper;
|
||||||
|
import org.xyzh.message.service.impl.MessageSendService;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息定时任务扫描器
|
||||||
|
*
|
||||||
|
* @description 定时扫描待发送的定时消息并触发发送
|
||||||
|
* @filename MessageScheduler.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class MessageScheduler {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MessageScheduler.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageMapper messageMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageSendService messageSendService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描待发送的定时消息
|
||||||
|
* 每分钟执行一次
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 * * * * ?")
|
||||||
|
public void scanPendingScheduledMessages() {
|
||||||
|
try {
|
||||||
|
logger.debug("开始扫描待发送的定时消息...");
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
// 查询满足条件的消息:
|
||||||
|
// 1. status=pending(待发送)
|
||||||
|
// 2. sendMode=scheduled(定时发送)
|
||||||
|
// 3. scheduledTime <= 当前时间
|
||||||
|
// 4. deleted=0(未删除)
|
||||||
|
List<TbSysMessage> messages = messageMapper.selectPendingScheduledMessages(now);
|
||||||
|
|
||||||
|
if (messages.isEmpty()) {
|
||||||
|
logger.debug("没有待发送的定时消息");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("发现 {} 条待发送的定时消息", messages.size());
|
||||||
|
|
||||||
|
// 处理每条消息
|
||||||
|
for (TbSysMessage message : messages) {
|
||||||
|
try {
|
||||||
|
// 使用CAS(Compare-And-Set)更新状态,防止并发重复触发
|
||||||
|
boolean updated = messageMapper.compareAndSetStatus(
|
||||||
|
message.getMessageID(),
|
||||||
|
"pending", // 期望的当前状态
|
||||||
|
"sending" // 新状态
|
||||||
|
) > 0;
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
logger.warn("消息 {} 状态已被修改,跳过处理", message.getMessageID());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新实际发送时间和状态
|
||||||
|
message.setActualSendTime(new Date());
|
||||||
|
message.setStatus("sending");
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
|
||||||
|
// 异步发送消息
|
||||||
|
messageSendService.sendMessageAsync(message.getMessageID());
|
||||||
|
|
||||||
|
logger.info("定时消息 {} 已触发发送", message.getMessageID());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理定时消息失败:{}", message.getMessageID(), e);
|
||||||
|
handleSendError(message, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("定时消息扫描完成,已处理 {} 条消息", messages.size());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("扫描定时消息时发生错误", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理发送失败,支持重试机制
|
||||||
|
*
|
||||||
|
* @param message 消息对象
|
||||||
|
* @param e 异常信息
|
||||||
|
*/
|
||||||
|
private void handleSendError(TbSysMessage message, Exception e) {
|
||||||
|
try {
|
||||||
|
int retryCount = message.getRetryCount() != null ? message.getRetryCount() : 0;
|
||||||
|
int maxRetryCount = message.getMaxRetryCount() != null ? message.getMaxRetryCount() : 3;
|
||||||
|
|
||||||
|
if (retryCount < maxRetryCount) {
|
||||||
|
// 未达到最大重试次数,增加重试计数
|
||||||
|
message.setRetryCount(retryCount + 1);
|
||||||
|
message.setStatus("pending"); // 重新置为待发送状态
|
||||||
|
|
||||||
|
// 设置下次重试时间(5分钟后)
|
||||||
|
LocalDateTime nextRetryTime = LocalDateTime.now().plusMinutes(5);
|
||||||
|
message.setScheduledTime(convertToDate(nextRetryTime));
|
||||||
|
|
||||||
|
message.setLastError(e.getMessage());
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
|
||||||
|
logger.warn("消息 {} 发送失败,将在5分钟后重试 ({}/{})",
|
||||||
|
message.getMessageID(), retryCount + 1, maxRetryCount);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// 超过最大重试次数,标记为失败
|
||||||
|
message.setStatus("failed");
|
||||||
|
message.setLastError("超过最大重试次数:" + e.getMessage());
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
|
||||||
|
logger.error("消息 {} 发送失败且已超过最大重试次数", message.getMessageID());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("处理发送错误时发生异常", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将LocalDateTime转换为Date
|
||||||
|
*/
|
||||||
|
private Date convertToDate(LocalDateTime localDateTime) {
|
||||||
|
return java.sql.Timestamp.valueOf(localDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每小时清理一次已取消和已失败的旧消息(可选功能)
|
||||||
|
* 将超过30天的已取消/失败消息标记为删除
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 0 * * * ?")
|
||||||
|
public void cleanupOldMessages() {
|
||||||
|
try {
|
||||||
|
logger.debug("开始清理旧消息...");
|
||||||
|
|
||||||
|
// 计算30天前的时间
|
||||||
|
LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);
|
||||||
|
|
||||||
|
// TODO: 实现清理逻辑
|
||||||
|
// 清理条件:
|
||||||
|
// 1. status in ('cancelled', 'failed')
|
||||||
|
// 2. updateTime < 30天前
|
||||||
|
// 3. deleted = 0
|
||||||
|
|
||||||
|
logger.debug("旧消息清理完成");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("清理旧消息时发生错误", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每天凌晨生成消息统计报告(可选功能)
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 0 0 * * ?")
|
||||||
|
public void generateDailyReport() {
|
||||||
|
try {
|
||||||
|
logger.info("开始生成每日消息统计报告...");
|
||||||
|
|
||||||
|
// TODO: 实现统计报告生成逻辑
|
||||||
|
// 统计内容:
|
||||||
|
// 1. 今日发送消息总数
|
||||||
|
// 2. 发送成功率
|
||||||
|
// 3. 已读率
|
||||||
|
// 4. 各发送方式的使用情况
|
||||||
|
|
||||||
|
logger.info("每日消息统计报告生成完成");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("生成每日报告时发生错误", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
package org.xyzh.message.service.impl;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.xyzh.common.dto.message.TbSysMessage;
|
||||||
|
import org.xyzh.common.dto.message.TbSysMessageUser;
|
||||||
|
import org.xyzh.common.dto.user.TbSysUser;
|
||||||
|
import org.xyzh.common.utils.EmailUtils;
|
||||||
|
import org.xyzh.common.utils.SmsUtils;
|
||||||
|
import org.xyzh.message.mapper.MessageMapper;
|
||||||
|
import org.xyzh.message.mapper.MessageUserMapper;
|
||||||
|
import org.xyzh.system.mapper.UserMapper;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息发送服务
|
||||||
|
*
|
||||||
|
* @description 异步发送消息的服务类
|
||||||
|
* @filename MessageSendService.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MessageSendService {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MessageSendService.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageMapper messageMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageUserMapper messageUserMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserMapper userMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EmailUtils emailUtils;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SmsUtils smsUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步发送消息
|
||||||
|
*
|
||||||
|
* @param messageID 消息ID
|
||||||
|
*/
|
||||||
|
@Async("messageAsyncExecutor")
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void sendMessageAsync(String messageID) {
|
||||||
|
try {
|
||||||
|
logger.info("开始发送消息:{}", messageID);
|
||||||
|
|
||||||
|
// 1. 查询消息
|
||||||
|
TbSysMessage message = messageMapper.selectMessageById(messageID);
|
||||||
|
|
||||||
|
if (message == null || message.getDeleted()) {
|
||||||
|
logger.error("消息不存在:{}", messageID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查询所有待发送的用户消息
|
||||||
|
TbSysMessageUser filter = new TbSysMessageUser();
|
||||||
|
filter.setMessageID(messageID);
|
||||||
|
filter.setSendStatus("pending");
|
||||||
|
filter.setDeleted(false);
|
||||||
|
List<TbSysMessageUser> userMessages = messageUserMapper.selectPendingUserMessages(filter);
|
||||||
|
|
||||||
|
if (userMessages.isEmpty()) {
|
||||||
|
logger.warn("没有待发送的用户消息:{}", messageID);
|
||||||
|
updateMessageStatus(message, "sent");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 遍历发送
|
||||||
|
int successCount = 0;
|
||||||
|
int failedCount = 0;
|
||||||
|
|
||||||
|
for (TbSysMessageUser userMessage : userMessages) {
|
||||||
|
try {
|
||||||
|
// 查询用户信息
|
||||||
|
TbSysUser user = userMapper.selectUserById(userMessage.getUserID());
|
||||||
|
if (user == null || user.getDeleted()) {
|
||||||
|
logger.warn("用户不存在:{}", userMessage.getUserID());
|
||||||
|
updateUserMessageStatus(userMessage.getID(), "failed", "用户不存在");
|
||||||
|
failedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据发送方式发送消息
|
||||||
|
String[] methods = userMessage.getSendMethod().split(",");
|
||||||
|
boolean sent = false;
|
||||||
|
|
||||||
|
for (String method : methods) {
|
||||||
|
method = method.trim();
|
||||||
|
try {
|
||||||
|
switch (method) {
|
||||||
|
case "system":
|
||||||
|
// 系统消息已在数据库中,无需额外操作
|
||||||
|
sent = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "email":
|
||||||
|
sent = sendEmail(user, message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "sms":
|
||||||
|
sent = sendSms(user, message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.warn("未知的发送方式:{}", method);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
break; // 任意一种方式成功即可
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("发送消息失败 [{}] - 用户:{}, 方式:{}", messageID, user.getUsername(), method, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户消息发送状态
|
||||||
|
if (sent) {
|
||||||
|
updateUserMessageStatus(userMessage.getID(), "success", null);
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
updateUserMessageStatus(userMessage.getID(), "failed", "所有发送方式均失败");
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理用户消息失败:{}", userMessage.getID(), e);
|
||||||
|
updateUserMessageStatus(userMessage.getID(), "failed", e.getMessage());
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 更新消息统计信息
|
||||||
|
int sentCount = successCount + failedCount;
|
||||||
|
messageMapper.updateStatistics(messageID, sentCount, successCount, failedCount);
|
||||||
|
|
||||||
|
// 5. 更新消息状态
|
||||||
|
if (failedCount == userMessages.size()) {
|
||||||
|
// 全部失败
|
||||||
|
updateMessageStatus(message, "failed");
|
||||||
|
} else {
|
||||||
|
// 至少有一个成功
|
||||||
|
updateMessageStatus(message, "sent");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("消息发送完成:{} - 成功:{}, 失败:{}", messageID, successCount, failedCount);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("发送消息异常:{}", messageID, e);
|
||||||
|
// 更新消息状态为失败
|
||||||
|
try {
|
||||||
|
TbSysMessage message = messageMapper.selectMessageById(messageID);
|
||||||
|
if (message != null && !message.getDeleted()) {
|
||||||
|
message.setStatus("failed");
|
||||||
|
message.setLastError(e.getMessage());
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("更新消息状态失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮件
|
||||||
|
*
|
||||||
|
* @param user 用户
|
||||||
|
* @param message 消息
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
private boolean sendEmail(TbSysUser user, TbSysMessage message) {
|
||||||
|
try {
|
||||||
|
if (user.getEmail() == null || user.getEmail().trim().isEmpty()) {
|
||||||
|
logger.warn("用户 {} 没有邮箱地址", user.getUsername());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建HTML邮件内容
|
||||||
|
String htmlContent = buildEmailHtml(message);
|
||||||
|
|
||||||
|
// 发送邮件
|
||||||
|
boolean result = emailUtils.sendHtmlEmail(
|
||||||
|
user.getEmail(),
|
||||||
|
message.getTitle(),
|
||||||
|
htmlContent
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
logger.info("邮件发送成功 - 用户:{}, 邮箱:{}", user.getUsername(), user.getEmail());
|
||||||
|
} else {
|
||||||
|
logger.warn("邮件发送失败 - 用户:{}, 邮箱:{}", user.getUsername(), user.getEmail());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("发送邮件异常 - 用户:{}", user.getUsername(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信
|
||||||
|
*
|
||||||
|
* @param user 用户
|
||||||
|
* @param message 消息
|
||||||
|
* @return 是否成功
|
||||||
|
*/
|
||||||
|
private boolean sendSms(TbSysUser user, TbSysMessage message) {
|
||||||
|
try {
|
||||||
|
if (user.getPhone() == null || user.getPhone().trim().isEmpty()) {
|
||||||
|
logger.warn("用户 {} 没有手机号", user.getUsername());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 短信内容(限制长度)
|
||||||
|
String smsContent = message.getTitle();
|
||||||
|
if (message.getContent() != null && message.getContent().length() < 50) {
|
||||||
|
smsContent += ":" + message.getContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送短信
|
||||||
|
boolean result = smsUtils.sendSms(user.getPhone(), smsContent, "xx");
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
logger.info("短信发送成功 - 用户:{}, 手机:{}", user.getUsername(), user.getPhone());
|
||||||
|
} else {
|
||||||
|
logger.warn("短信发送失败 - 用户:{}, 手机:{}", user.getUsername(), user.getPhone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("发送短信异常 - 用户:{}", user.getUsername(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建邮件HTML内容
|
||||||
|
*/
|
||||||
|
private String buildEmailHtml(TbSysMessage message) {
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
html.append("<!DOCTYPE html>");
|
||||||
|
html.append("<html>");
|
||||||
|
html.append("<head>");
|
||||||
|
html.append("<meta charset=\"UTF-8\">");
|
||||||
|
html.append("<style>");
|
||||||
|
html.append("body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }");
|
||||||
|
html.append(".container { max-width: 600px; margin: 20px auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }");
|
||||||
|
html.append(".header { background-color: #c8232c; color: white; padding: 15px; border-radius: 5px 5px 0 0; }");
|
||||||
|
html.append(".content { padding: 20px; background-color: #f9f9f9; }");
|
||||||
|
html.append(".footer { text-align: center; padding: 15px; font-size: 12px; color: #999; }");
|
||||||
|
html.append(".priority-urgent { border-left: 4px solid #ff0000; }");
|
||||||
|
html.append(".priority-important { border-left: 4px solid #ff9900; }");
|
||||||
|
html.append(".priority-normal { border-left: 4px solid #00aa00; }");
|
||||||
|
html.append("</style>");
|
||||||
|
html.append("</head>");
|
||||||
|
html.append("<body>");
|
||||||
|
html.append("<div class=\"container priority-").append(message.getPriority()).append("\">");
|
||||||
|
html.append("<div class=\"header\">");
|
||||||
|
html.append("<h2>").append(message.getTitle()).append("</h2>");
|
||||||
|
html.append("</div>");
|
||||||
|
html.append("<div class=\"content\">");
|
||||||
|
html.append(message.getContent());
|
||||||
|
html.append("</div>");
|
||||||
|
html.append("<div class=\"footer\">");
|
||||||
|
html.append("<p>发送人:").append(message.getSenderName()).append(" (").append(message.getSenderDeptName()).append(")</p>");
|
||||||
|
html.append("<p>发送时间:").append(message.getActualSendTime() != null ? message.getActualSendTime().toString() : "").append("</p>");
|
||||||
|
html.append("<p>此邮件由系统自动发送,请勿回复。</p>");
|
||||||
|
html.append("</div>");
|
||||||
|
html.append("</div>");
|
||||||
|
html.append("</body>");
|
||||||
|
html.append("</html>");
|
||||||
|
return html.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新消息状态
|
||||||
|
*/
|
||||||
|
private void updateMessageStatus(TbSysMessage message, String status) {
|
||||||
|
try {
|
||||||
|
message.setStatus(status);
|
||||||
|
if ("sent".equals(status)) {
|
||||||
|
message.setActualSendTime(new Date());
|
||||||
|
}
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("更新消息状态失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户消息发送状态
|
||||||
|
*/
|
||||||
|
private void updateUserMessageStatus(String id, String status, String failReason) {
|
||||||
|
try {
|
||||||
|
messageUserMapper.updateSendStatus(id, status, failReason);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("更新用户消息状态失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,746 @@
|
|||||||
|
package org.xyzh.message.service.impl;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.xyzh.api.message.MessageService;
|
||||||
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
|
import org.xyzh.common.core.page.PageDomain;
|
||||||
|
import org.xyzh.common.core.page.PageParam;
|
||||||
|
import org.xyzh.common.dto.message.*;
|
||||||
|
import org.xyzh.common.utils.IDUtils;
|
||||||
|
import org.xyzh.message.mapper.MessageMapper;
|
||||||
|
import org.xyzh.message.mapper.MessageTargetMapper;
|
||||||
|
import org.xyzh.message.mapper.MessageUserMapper;
|
||||||
|
import org.xyzh.system.mapper.DepartmentMapper;
|
||||||
|
import org.xyzh.system.mapper.UserMapper;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息服务实现类
|
||||||
|
*
|
||||||
|
* @description 消息通知模块的服务实现
|
||||||
|
* @filename MessageServiceImpl.java
|
||||||
|
* @author Claude
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class MessageServiceImpl implements MessageService {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MessageServiceImpl.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageMapper messageMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageTargetMapper messageTargetMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageUserMapper messageUserMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DepartmentMapper departmentMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserMapper userMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MessageSendService messageSendService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建消息
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ResultDomain<TbSysMessage> createMessage(TbSysMessage message) {
|
||||||
|
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
// 1. 获取当前用户信息(从Session或SecurityContext获取)
|
||||||
|
String currentUserID = getCurrentUserID();
|
||||||
|
String currentDeptID = getCurrentUserDeptID();
|
||||||
|
|
||||||
|
// 2. 设置消息主体基本信息
|
||||||
|
if (message.getID() == null) {
|
||||||
|
message.setID(IDUtils.generateID());
|
||||||
|
}
|
||||||
|
if (message.getMessageID() == null) {
|
||||||
|
message.setMessageID(IDUtils.generateID());
|
||||||
|
}
|
||||||
|
message.setSenderID(currentUserID);
|
||||||
|
message.setSenderDeptID(currentDeptID);
|
||||||
|
|
||||||
|
// 设置状态
|
||||||
|
if (message.getStatus() == null) {
|
||||||
|
if ("immediate".equals(message.getSendMode())) {
|
||||||
|
message.setStatus("sending");
|
||||||
|
} else {
|
||||||
|
message.setStatus("pending");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.getTargetUserCount() == null) {
|
||||||
|
message.setTargetUserCount(0);
|
||||||
|
}
|
||||||
|
if (message.getRetryCount() == null) {
|
||||||
|
message.setRetryCount(0);
|
||||||
|
}
|
||||||
|
if (message.getMaxRetryCount() == null) {
|
||||||
|
message.setMaxRetryCount(3);
|
||||||
|
}
|
||||||
|
message.setCreator(currentUserID);
|
||||||
|
message.setCreateTime(new Date());
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
message.setDeleted(false);
|
||||||
|
|
||||||
|
// 保存消息主体
|
||||||
|
messageMapper.insertMessage(message);
|
||||||
|
|
||||||
|
// 3. 保存接收对象配置
|
||||||
|
List<TbSysMessageTarget> targets = message.getTargets();
|
||||||
|
if (targets != null && !targets.isEmpty()) {
|
||||||
|
for (TbSysMessageTarget target : targets) {
|
||||||
|
// 权限校验:scopeDeptID必须是当前部门或子部门
|
||||||
|
if (!isCurrentOrSubDept(currentDeptID, target.getScopeDeptID())) {
|
||||||
|
rt.fail("无权向该部门发送消息");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.getID() == null) {
|
||||||
|
target.setID(IDUtils.generateID());
|
||||||
|
}
|
||||||
|
target.setMessageID(message.getMessageID());
|
||||||
|
target.setCreator(currentUserID);
|
||||||
|
target.setCreateTime(new Date());
|
||||||
|
target.setUpdateTime(new Date());
|
||||||
|
target.setDeleted(false);
|
||||||
|
}
|
||||||
|
messageTargetMapper.batchInsert(targets);
|
||||||
|
|
||||||
|
// 4. 解析接收对象,生成用户消息列表
|
||||||
|
List<TbSysMessageUser> userMessages = resolveTargetUsers(message.getMessageID(), targets, currentUserID);
|
||||||
|
if (!userMessages.isEmpty()) {
|
||||||
|
messageUserMapper.batchInsert(userMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 更新目标用户总数
|
||||||
|
message.setTargetUserCount(userMessages.size());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
|
||||||
|
// 6. 如果是立即发送,异步发送消息
|
||||||
|
if ("immediate".equals(message.getSendMode())) {
|
||||||
|
messageSendService.sendMessageAsync(message.getMessageID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rt.success("创建成功", message);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("创建消息失败", e);
|
||||||
|
rt.fail("创建消息失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新消息(仅允许更新草稿状态的消息)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ResultDomain<TbSysMessage> updateMessage(TbSysMessage message) {
|
||||||
|
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
// 查询原消息
|
||||||
|
TbSysMessage existingMessage = messageMapper.selectMessageById(message.getMessageID());
|
||||||
|
if (existingMessage == null || existingMessage.getDeleted()) {
|
||||||
|
rt.fail("消息不存在");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只允许更新草稿状态的消息
|
||||||
|
if (!"draft".equals(existingMessage.getStatus())) {
|
||||||
|
rt.fail("只能更新草稿状态的消息");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
message.setUpdater(getCurrentUserID());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
rt.success("更新成功", message);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("更新消息失败", e);
|
||||||
|
rt.fail("更新消息失败:"+e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消息(逻辑删除)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ResultDomain<TbSysMessage> deleteMessage(String messageID) {
|
||||||
|
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
TbSysMessage message = messageMapper.selectMessageById(messageID);
|
||||||
|
|
||||||
|
if (message == null || message.getDeleted()) {
|
||||||
|
rt.fail("消息不存在");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = messageMapper.deleteMessage(messageID);
|
||||||
|
if (result > 0) {
|
||||||
|
// 同时删除关联的接收对象和用户消息
|
||||||
|
messageTargetMapper.deleteByMessageID(messageID);
|
||||||
|
rt.success("删除成功", message);
|
||||||
|
} else {
|
||||||
|
rt.fail("删除失败");
|
||||||
|
}
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("删除消息失败", e);
|
||||||
|
rt.fail("删除消息失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询消息详情
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ResultDomain<MessageVO> getMessageById(String messageID) {
|
||||||
|
ResultDomain<MessageVO> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
MessageVO messageVO = messageMapper.selectMessageDetail(messageID);
|
||||||
|
if (messageVO == null) {
|
||||||
|
rt.fail("消息不存在");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询接收对象列表
|
||||||
|
List<TbSysMessageTarget> targets = messageTargetMapper.selectByMessageID(messageID);
|
||||||
|
messageVO.setTargets(targets);
|
||||||
|
|
||||||
|
// 查询用户接收记录
|
||||||
|
List<MessageUserVO> userMessages = messageUserMapper.selectByMessageID(messageID);
|
||||||
|
messageVO.setUserMessages(userMessages);
|
||||||
|
|
||||||
|
rt.success("查询成功", messageVO);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("查询消息详情失败", e);
|
||||||
|
rt.fail("查询消息详情失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询消息列表(管理端)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ResultDomain<MessageVO> getMessagePage(TbSysMessage filter, PageParam pageParam) {
|
||||||
|
ResultDomain<MessageVO> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
if (filter == null) {
|
||||||
|
filter = new TbSysMessage();
|
||||||
|
}
|
||||||
|
filter.setDeleted(false);
|
||||||
|
|
||||||
|
if (pageParam == null) {
|
||||||
|
pageParam = new PageParam();
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentDeptID = getCurrentUserDeptID();
|
||||||
|
|
||||||
|
List<MessageVO> list = messageMapper.selectMessagePage(filter, currentDeptID);
|
||||||
|
int total = messageMapper.countMessage(filter, currentDeptID);
|
||||||
|
|
||||||
|
PageDomain<MessageVO> pageDomain = new PageDomain<>();
|
||||||
|
pageDomain.setDataList(list);
|
||||||
|
pageParam.setTotalElements(total);
|
||||||
|
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
|
||||||
|
pageDomain.setPageParam(pageParam);
|
||||||
|
|
||||||
|
rt.success("查询成功", pageDomain);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("查询消息列表失败", e);
|
||||||
|
rt.fail("查询消息列表失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ResultDomain<TbSysMessage> sendMessage(String messageID) {
|
||||||
|
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
TbSysMessage message = messageMapper.selectMessageById(messageID);
|
||||||
|
|
||||||
|
if (message == null || message.getDeleted()) {
|
||||||
|
rt.fail("消息不存在");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态为发送中
|
||||||
|
message.setStatus("sending");
|
||||||
|
message.setActualSendTime(new Date());
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
|
||||||
|
// 异步发送
|
||||||
|
messageSendService.sendMessageAsync(messageID);
|
||||||
|
|
||||||
|
rt.success("发送成功", message);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("发送消息失败", e);
|
||||||
|
rt.fail("发送消息失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立即发送(将定时消息改为立即发送)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ResultDomain<TbSysMessage> sendNow(String messageID) {
|
||||||
|
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
TbSysMessage message = messageMapper.selectMessageById(messageID);
|
||||||
|
|
||||||
|
if (message == null || message.getDeleted()) {
|
||||||
|
rt.fail("消息不存在");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"pending".equals(message.getStatus())) {
|
||||||
|
rt.fail("只能立即发送待发送状态的消息");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新为立即发送模式
|
||||||
|
message.setSendMode("immediate");
|
||||||
|
message.setStatus("sending");
|
||||||
|
message.setActualSendTime(new Date());
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
|
||||||
|
// 异步发送
|
||||||
|
messageSendService.sendMessageAsync(messageID);
|
||||||
|
|
||||||
|
rt.success("立即发送成功", message);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("立即发送消息失败", e);
|
||||||
|
rt.fail("立即发送消息失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消定时消息
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ResultDomain<TbSysMessage> cancelMessage(String messageID) {
|
||||||
|
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
TbSysMessage message = messageMapper.selectMessageById(messageID);
|
||||||
|
|
||||||
|
if (message == null || message.getDeleted()) {
|
||||||
|
rt.fail("消息不存在");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"pending".equals(message.getStatus())) {
|
||||||
|
rt.fail("只能取消待发送状态的消息");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.setStatus("cancelled");
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
|
||||||
|
rt.success("取消成功", message);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("取消消息失败", e);
|
||||||
|
rt.fail("取消消息失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改定时发送时间
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ResultDomain<TbSysMessage> rescheduleMessage(String messageID, Date scheduledTime) {
|
||||||
|
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
TbSysMessage message = messageMapper.selectMessageById(messageID);
|
||||||
|
|
||||||
|
if (message == null || message.getDeleted()) {
|
||||||
|
rt.fail("消息不存在");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"pending".equals(message.getStatus())) {
|
||||||
|
rt.fail("只能修改待发送状态的消息");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.setScheduledTime(scheduledTime);
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
|
||||||
|
rt.success("修改成功", message);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("修改定时时间失败", e);
|
||||||
|
rt.fail("修改定时时间失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试失败消息
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ResultDomain<TbSysMessage> retryMessage(String messageID) {
|
||||||
|
ResultDomain<TbSysMessage> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
TbSysMessage message = messageMapper.selectMessageById(messageID);
|
||||||
|
|
||||||
|
if (message == null || message.getDeleted()) {
|
||||||
|
rt.fail("消息不存在");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"failed".equals(message.getStatus())) {
|
||||||
|
rt.fail("只能重试失败状态的消息");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
message.setStatus("sending");
|
||||||
|
message.setRetryCount(message.getRetryCount() + 1);
|
||||||
|
message.setUpdateTime(new Date());
|
||||||
|
messageMapper.updateMessage(message);
|
||||||
|
|
||||||
|
// 异步发送
|
||||||
|
messageSendService.sendMessageAsync(messageID);
|
||||||
|
|
||||||
|
rt.success("重试成功", message);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("重试消息失败", e);
|
||||||
|
rt.fail("重试消息失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================== 用户消息相关方法 ==================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<MessageUserVO> getMyMessagesPage(MessageUserVO filter, PageParam pageParam) {
|
||||||
|
ResultDomain<MessageUserVO> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
if (filter == null) {
|
||||||
|
filter = new MessageUserVO();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageParam == null) {
|
||||||
|
pageParam = new PageParam();
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentUserID = getCurrentUserID();
|
||||||
|
|
||||||
|
// 使用新的动态查询方法
|
||||||
|
List<MessageUserVO> list = messageUserMapper.selectMyMessagesWithDynamicTargets(currentUserID, filter);
|
||||||
|
|
||||||
|
PageDomain<MessageUserVO> pageDomain = new PageDomain<>();
|
||||||
|
pageDomain.setDataList(list);
|
||||||
|
pageParam.setTotalElements(list.size());
|
||||||
|
pageParam.setTotalPages((int) Math.ceil((double) list.size() / pageParam.getPageSize()));
|
||||||
|
pageDomain.setPageParam(pageParam);
|
||||||
|
|
||||||
|
rt.success("查询成功", pageDomain);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("查询我的消息失败", e);
|
||||||
|
rt.fail("查询我的消息失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<MessageUserVO> getMyMessageDetail(String messageID) {
|
||||||
|
ResultDomain<MessageUserVO> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
String currentUserID = getCurrentUserID();
|
||||||
|
MessageUserVO messageUserVO = messageUserMapper.selectOrCreateUserMessage(currentUserID, messageID);
|
||||||
|
|
||||||
|
if (messageUserVO == null) {
|
||||||
|
rt.fail("消息不存在");
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户消息记录不存在(id 为 null),创建新记录
|
||||||
|
if (messageUserVO.getId() == null) {
|
||||||
|
logger.info("用户首次查看消息,创建用户消息记录:userID={}, messageID={}", currentUserID, messageID);
|
||||||
|
|
||||||
|
TbSysMessageUser userMessage = new TbSysMessageUser();
|
||||||
|
userMessage.setID(IDUtils.generateID());
|
||||||
|
userMessage.setMessageID(messageID);
|
||||||
|
userMessage.setUserID(currentUserID);
|
||||||
|
userMessage.setSendMethod("system"); // 默认发送方式
|
||||||
|
userMessage.setIsRead(false); // 初始为未读
|
||||||
|
userMessage.setSendStatus("success");
|
||||||
|
userMessage.setCreator(currentUserID);
|
||||||
|
userMessage.setCreateTime(new Date());
|
||||||
|
userMessage.setUpdateTime(new Date());
|
||||||
|
userMessage.setDeleted(false);
|
||||||
|
|
||||||
|
// 插入新记录
|
||||||
|
messageUserMapper.insertIfNotExists(userMessage);
|
||||||
|
|
||||||
|
// 重新查询以获取完整信息
|
||||||
|
messageUserVO = messageUserMapper.selectMyMessageDetail(currentUserID, messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
rt.success("查询成功", messageUserVO);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("查询消息详情失败", e);
|
||||||
|
rt.fail("查询消息详情失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ResultDomain<TbSysMessageUser> markAsRead(String messageID) {
|
||||||
|
ResultDomain<TbSysMessageUser> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
String currentUserID = getCurrentUserID();
|
||||||
|
|
||||||
|
// 先尝试更新已有记录
|
||||||
|
int result = messageUserMapper.markAsRead(currentUserID, messageID);
|
||||||
|
|
||||||
|
// 如果没有更新任何记录,说明用户消息记录不存在,需要先插入
|
||||||
|
if (result == 0) {
|
||||||
|
logger.info("用户消息记录不存在,创建新记录:userID={}, messageID={}", currentUserID, messageID);
|
||||||
|
|
||||||
|
TbSysMessageUser userMessage = new TbSysMessageUser();
|
||||||
|
userMessage.setID(IDUtils.generateID());
|
||||||
|
userMessage.setMessageID(messageID);
|
||||||
|
userMessage.setUserID(currentUserID);
|
||||||
|
userMessage.setSendMethod("system"); // 默认发送方式
|
||||||
|
userMessage.setIsRead(true); // 直接设置为已读
|
||||||
|
userMessage.setSendStatus("success");
|
||||||
|
userMessage.setCreator(currentUserID);
|
||||||
|
userMessage.setCreateTime(new Date());
|
||||||
|
userMessage.setUpdateTime(new Date());
|
||||||
|
userMessage.setDeleted(false);
|
||||||
|
|
||||||
|
// 插入新记录
|
||||||
|
int insertResult = messageUserMapper.insertIfNotExists(userMessage);
|
||||||
|
|
||||||
|
if (insertResult > 0) {
|
||||||
|
// 插入成功后再次标记为已读(设置 read_time)
|
||||||
|
messageUserMapper.markAsRead(currentUserID, messageID);
|
||||||
|
result = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result > 0) {
|
||||||
|
// 更新消息的已读数量
|
||||||
|
messageMapper.incrementReadCount(messageID);
|
||||||
|
TbSysMessageUser messageUser = new TbSysMessageUser();
|
||||||
|
rt.success("标记成功", messageUser);
|
||||||
|
} else {
|
||||||
|
rt.fail("标记失败");
|
||||||
|
}
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("标记已读失败", e);
|
||||||
|
rt.fail("标记已读失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ResultDomain<Integer> batchMarkAsRead(List<String> messageIDs) {
|
||||||
|
ResultDomain<Integer> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
String currentUserID = getCurrentUserID();
|
||||||
|
int count = messageUserMapper.batchMarkAsRead(currentUserID, messageIDs);
|
||||||
|
|
||||||
|
// 更新每条消息的已读数量
|
||||||
|
for (String messageID : messageIDs) {
|
||||||
|
messageMapper.incrementReadCount(messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
rt.success("批量标记成功", count);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("批量标记已读失败", e);
|
||||||
|
rt.fail("批量标记已读失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<Integer> getUnreadCount() {
|
||||||
|
ResultDomain<Integer> rt = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
String currentUserID = getCurrentUserID();
|
||||||
|
// 使用动态计算方法,统计用户应该看到的所有未读消息
|
||||||
|
Integer count = messageUserMapper.countUnreadWithDynamicTargets(currentUserID);
|
||||||
|
rt.success("查询成功", count != null ? count : 0);
|
||||||
|
return rt;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("查询未读数量失败", e);
|
||||||
|
rt.fail("查询未读数量失败:" + e.getMessage());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================== 辅助方法 ==================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<Map<String, Object>> getTargetDepts() {
|
||||||
|
ResultDomain<Map<String, Object>> rt = new ResultDomain<>();
|
||||||
|
// TODO: 实现获取可选部门树
|
||||||
|
rt.success("查询成功", new HashMap<>());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<Map<String, Object>> getTargetRoles() {
|
||||||
|
ResultDomain<Map<String, Object>> rt = new ResultDomain<>();
|
||||||
|
// TODO: 实现获取可选角色列表
|
||||||
|
rt.success("查询成功", new HashMap<>());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<Map<String, Object>> getTargetUsers() {
|
||||||
|
ResultDomain<Map<String, Object>> rt = new ResultDomain<>();
|
||||||
|
// TODO: 实现获取可选用户列表
|
||||||
|
rt.success("查询成功", new HashMap<>());
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================== 私有辅助方法 ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析接收对象,生成用户消息列表
|
||||||
|
*/
|
||||||
|
private List<TbSysMessageUser> resolveTargetUsers(String messageID, List<TbSysMessageTarget> targets, String creator) {
|
||||||
|
Set<String> userIDSet = new HashSet<>();
|
||||||
|
Map<String, String> userMethodMap = new HashMap<>();
|
||||||
|
|
||||||
|
for (TbSysMessageTarget target : targets) {
|
||||||
|
List<String> userIDs = new ArrayList<>();
|
||||||
|
|
||||||
|
switch (target.getTargetType()) {
|
||||||
|
case "dept":
|
||||||
|
// 查询该部门及所有子部门的用户
|
||||||
|
userIDs = userMapper.selectUserIdsByDeptId(target.getTargetID());
|
||||||
|
logger.info("部门 {} 解析到 {} 个用户", target.getTargetID(), userIDs.size());
|
||||||
|
break;
|
||||||
|
case "role":
|
||||||
|
// 查询scopeDeptID及子部门中该角色的用户
|
||||||
|
String scopeDeptID = target.getScopeDeptID();
|
||||||
|
if (scopeDeptID == null || scopeDeptID.isEmpty()) {
|
||||||
|
logger.warn("角色目标缺少 scopeDeptID,跳过:{}", target.getTargetID());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
userIDs = userMapper.selectUserIdsByDeptRole(scopeDeptID, target.getTargetID());
|
||||||
|
logger.info("部门 {} 中角色 {} 解析到 {} 个用户", scopeDeptID, target.getTargetID(), userIDs.size());
|
||||||
|
break;
|
||||||
|
case "user":
|
||||||
|
userIDs.add(target.getTargetID());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String userID : userIDs) {
|
||||||
|
userIDSet.add(userID);
|
||||||
|
userMethodMap.put(userID, target.getSendMethod());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成用户消息列表
|
||||||
|
List<TbSysMessageUser> userMessages = new ArrayList<>();
|
||||||
|
for (String userID : userIDSet) {
|
||||||
|
TbSysMessageUser userMessage = new TbSysMessageUser();
|
||||||
|
userMessage.setID(IDUtils.generateID());
|
||||||
|
userMessage.setMessageID(messageID);
|
||||||
|
userMessage.setUserID(userID);
|
||||||
|
userMessage.setSendMethod(userMethodMap.get(userID));
|
||||||
|
userMessage.setIsRead(false);
|
||||||
|
userMessage.setSendStatus("pending");
|
||||||
|
userMessage.setCreator(creator);
|
||||||
|
userMessage.setCreateTime(new Date());
|
||||||
|
userMessage.setUpdateTime(new Date());
|
||||||
|
userMessage.setDeleted(false);
|
||||||
|
userMessages.add(userMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("消息 {} 共解析到 {} 个目标用户", messageID, userMessages.size());
|
||||||
|
return userMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查目标部门是否是当前部门或子部门
|
||||||
|
*/
|
||||||
|
private boolean isCurrentOrSubDept(String currentDeptID, String targetDeptID) {
|
||||||
|
// TODO: 实现部门层级检查
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户ID
|
||||||
|
*/
|
||||||
|
private String getCurrentUserID() {
|
||||||
|
// TODO: 从SecurityContext或Session获取
|
||||||
|
return "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户部门ID
|
||||||
|
*/
|
||||||
|
private String getCurrentUserDeptID() {
|
||||||
|
// TODO: 从SecurityContext或Session获取
|
||||||
|
return "root_department";
|
||||||
|
}
|
||||||
|
}
|
||||||
44
schoolNewsServ/message/src/main/resources/application.yml
Normal file
44
schoolNewsServ/message/src/main/resources/application.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Message模块配置文件
|
||||||
|
# 此配置文件定义message模块独立运行时的配置
|
||||||
|
# 如需作为微服务集成,请参考admin模块的bootstrap.yml配置
|
||||||
|
|
||||||
|
# 消息模块配置
|
||||||
|
message:
|
||||||
|
# 定时任务扫描配置
|
||||||
|
scheduler:
|
||||||
|
# 扫描频率(cron表达式),默认每分钟扫描一次
|
||||||
|
cron: "0 * * * * ?"
|
||||||
|
# 是否启用定时任务扫描
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# 消息发送配置
|
||||||
|
send:
|
||||||
|
# 异步发送线程池大小
|
||||||
|
thread-pool-size: 10
|
||||||
|
# 批量发送每批次大小
|
||||||
|
batch-size: 100
|
||||||
|
|
||||||
|
# Spring Boot配置(如果需要独立运行)
|
||||||
|
# server:
|
||||||
|
# port: 8087
|
||||||
|
#
|
||||||
|
# spring:
|
||||||
|
# application:
|
||||||
|
# name: message-service
|
||||||
|
#
|
||||||
|
# datasource:
|
||||||
|
# url: jdbc:mysql://localhost:3306/school_news?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
||||||
|
# username: root
|
||||||
|
# password: your_password
|
||||||
|
# driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
# hikari:
|
||||||
|
# maximum-pool-size: 20
|
||||||
|
# minimum-idle: 5
|
||||||
|
# connection-timeout: 30000
|
||||||
|
#
|
||||||
|
# mybatis-plus:
|
||||||
|
# mapper-locations: classpath:mapper/*.xml
|
||||||
|
# type-aliases-package: org.xyzh.common.dto.message
|
||||||
|
# configuration:
|
||||||
|
# map-underscore-to-camel-case: true
|
||||||
|
# log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
<?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.message.mapper.MessageMapper">
|
||||||
|
|
||||||
|
<!-- Result Map -->
|
||||||
|
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.message.TbSysMessage">
|
||||||
|
<id column="id" property="id" jdbcType="VARCHAR"/>
|
||||||
|
<result column="message_id" property="messageID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="title" property="title" jdbcType="VARCHAR"/>
|
||||||
|
<result column="content" property="content" jdbcType="VARCHAR"/>
|
||||||
|
<result column="message_type" property="messageType" jdbcType="VARCHAR"/>
|
||||||
|
<result column="priority" property="priority" jdbcType="VARCHAR"/>
|
||||||
|
<result column="sender_id" property="senderID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="sender_name" property="senderName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="sender_dept_id" property="senderDeptID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="sender_dept_name" property="senderDeptName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="send_mode" property="sendMode" jdbcType="VARCHAR"/>
|
||||||
|
<result column="scheduled_time" property="scheduledTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="actual_send_time" property="actualSendTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="status" property="status" jdbcType="VARCHAR"/>
|
||||||
|
<result column="target_user_count" property="targetUserCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="sent_count" property="sentCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="success_count" property="successCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="failed_count" property="failedCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="read_count" property="readCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="retry_count" property="retryCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="max_retry_count" property="maxRetryCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="last_error" property="lastError" jdbcType="VARCHAR"/>
|
||||||
|
<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="BOOLEAN"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<resultMap id="MessageVOMap" type="org.xyzh.common.dto.message.MessageVO">
|
||||||
|
<id column="id" property="id" jdbcType="VARCHAR"/>
|
||||||
|
<result column="message_id" property="messageID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="title" property="title" jdbcType="VARCHAR"/>
|
||||||
|
<result column="content" property="content" jdbcType="VARCHAR"/>
|
||||||
|
<result column="message_type" property="messageType" jdbcType="VARCHAR"/>
|
||||||
|
<result column="priority" property="priority" jdbcType="VARCHAR"/>
|
||||||
|
<result column="sender_id" property="senderID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="sender_name" property="senderName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="sender_dept_id" property="senderDeptID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="sender_dept_name" property="senderDeptName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="send_mode" property="sendMode" jdbcType="VARCHAR"/>
|
||||||
|
<result column="scheduled_time" property="scheduledTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="actual_send_time" property="actualSendTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="status" property="status" jdbcType="VARCHAR"/>
|
||||||
|
<result column="target_user_count" property="targetUserCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="sent_count" property="sentCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="success_count" property="successCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="failed_count" property="failedCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="read_count" property="readCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="retry_count" property="retryCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="max_retry_count" property="maxRetryCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="last_error" property="lastError" jdbcType="VARCHAR"/>
|
||||||
|
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<!-- 分页查询消息列表(带权限过滤) -->
|
||||||
|
<select id="selectMessagePage" resultMap="MessageVOMap">
|
||||||
|
SELECT
|
||||||
|
m.*
|
||||||
|
FROM tb_sys_message m
|
||||||
|
WHERE m.deleted = 0
|
||||||
|
<if test="filter != null">
|
||||||
|
<if test="filter.title != null and filter.title != ''">
|
||||||
|
AND m.title LIKE CONCAT('%', #{filter.title}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="filter.messageType != null and filter.messageType != ''">
|
||||||
|
AND m.message_type = #{filter.messageType}
|
||||||
|
</if>
|
||||||
|
<if test="filter.status != null and filter.status != ''">
|
||||||
|
AND m.status = #{filter.status}
|
||||||
|
</if>
|
||||||
|
<if test="filter.sendMode != null and filter.sendMode != ''">
|
||||||
|
AND m.send_mode = #{filter.sendMode}
|
||||||
|
</if>
|
||||||
|
<if test="filter.priority != null and filter.priority != ''">
|
||||||
|
AND m.priority = #{filter.priority}
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<!-- 权限过滤:只能查看自己部门及子部门的消息 -->
|
||||||
|
AND (
|
||||||
|
m.sender_dept_id = #{currentUserDeptID}
|
||||||
|
OR m.sender_dept_id IN (
|
||||||
|
WITH RECURSIVE dept_tree AS (
|
||||||
|
SELECT dept_id FROM tb_sys_dept
|
||||||
|
WHERE dept_id = #{currentUserDeptID} AND deleted = 0
|
||||||
|
UNION ALL
|
||||||
|
SELECT d.dept_id FROM tb_sys_dept d
|
||||||
|
INNER JOIN dept_tree dt ON d.parent_id = dt.dept_id
|
||||||
|
WHERE d.deleted = 0
|
||||||
|
)
|
||||||
|
SELECT dept_id FROM dept_tree
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ORDER BY m.create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询消息详情 -->
|
||||||
|
<select id="selectMessageDetail" resultMap="MessageVOMap">
|
||||||
|
SELECT *
|
||||||
|
FROM tb_sys_message
|
||||||
|
WHERE message_id = #{messageID}
|
||||||
|
AND deleted = 0
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询待发送的定时消息 -->
|
||||||
|
<select id="selectPendingScheduledMessages" resultMap="BaseResultMap">
|
||||||
|
SELECT *
|
||||||
|
FROM tb_sys_message
|
||||||
|
WHERE status = 'pending'
|
||||||
|
AND send_mode = 'scheduled'
|
||||||
|
AND scheduled_time <![CDATA[ <= ]]> #{currentTime}
|
||||||
|
AND deleted = 0
|
||||||
|
ORDER BY scheduled_time ASC
|
||||||
|
LIMIT 100
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- CAS更新消息状态 -->
|
||||||
|
<update id="compareAndSetStatus">
|
||||||
|
UPDATE tb_sys_message
|
||||||
|
SET status = #{newStatus},
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE message_id = #{messageID}
|
||||||
|
AND status = #{expectedStatus}
|
||||||
|
AND deleted = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 更新消息统计信息 -->
|
||||||
|
<update id="updateStatistics">
|
||||||
|
UPDATE tb_sys_message
|
||||||
|
SET sent_count = #{sentCount},
|
||||||
|
success_count = #{successCount},
|
||||||
|
failed_count = #{failedCount},
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE message_id = #{messageID}
|
||||||
|
AND deleted = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 更新已读数量 -->
|
||||||
|
<update id="incrementReadCount">
|
||||||
|
UPDATE tb_sys_message
|
||||||
|
SET read_count = read_count + 1,
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE message_id = #{messageID}
|
||||||
|
AND deleted = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 根据消息ID查询消息 -->
|
||||||
|
<select id="selectMessageById" resultMap="BaseResultMap">
|
||||||
|
SELECT *
|
||||||
|
FROM tb_sys_message
|
||||||
|
WHERE message_id = #{messageID}
|
||||||
|
AND deleted = 0
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 插入消息 -->
|
||||||
|
<insert id="insertMessage">
|
||||||
|
INSERT INTO tb_sys_message
|
||||||
|
(id, message_id, title, content, message_type, priority, sender_id, sender_dept_id,
|
||||||
|
send_mode, scheduled_time, status, target_user_count, retry_count, max_retry_count,
|
||||||
|
creator, create_time, update_time, deleted)
|
||||||
|
VALUES
|
||||||
|
(#{message.id}, #{message.messageID}, #{message.title}, #{message.content},
|
||||||
|
#{message.messageType}, #{message.priority}, #{message.senderID}, #{message.senderDeptID},
|
||||||
|
#{message.sendMode}, #{message.scheduledTime}, #{message.status}, #{message.targetUserCount},
|
||||||
|
#{message.retryCount}, #{message.maxRetryCount}, #{message.creator}, #{message.createTime},
|
||||||
|
#{message.updateTime}, #{message.deleted})
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 更新消息 -->
|
||||||
|
<update id="updateMessage">
|
||||||
|
UPDATE tb_sys_message
|
||||||
|
<set>
|
||||||
|
<if test="message.title != null and message.title != ''">
|
||||||
|
title = #{message.title},
|
||||||
|
</if>
|
||||||
|
<if test="message.content != null">
|
||||||
|
content = #{message.content},
|
||||||
|
</if>
|
||||||
|
<if test="message.messageType != null and message.messageType != ''">
|
||||||
|
message_type = #{message.messageType},
|
||||||
|
</if>
|
||||||
|
<if test="message.priority != null and message.priority != ''">
|
||||||
|
priority = #{message.priority},
|
||||||
|
</if>
|
||||||
|
<if test="message.sendMode != null and message.sendMode != ''">
|
||||||
|
send_mode = #{message.sendMode},
|
||||||
|
</if>
|
||||||
|
<if test="message.scheduledTime != null">
|
||||||
|
scheduled_time = #{message.scheduledTime},
|
||||||
|
</if>
|
||||||
|
<if test="message.actualSendTime != null">
|
||||||
|
actual_send_time = #{message.actualSendTime},
|
||||||
|
</if>
|
||||||
|
<if test="message.status != null and message.status != ''">
|
||||||
|
status = #{message.status},
|
||||||
|
</if>
|
||||||
|
<if test="message.targetUserCount != null">
|
||||||
|
target_user_count = #{message.targetUserCount},
|
||||||
|
</if>
|
||||||
|
<if test="message.retryCount != null">
|
||||||
|
retry_count = #{message.retryCount},
|
||||||
|
</if>
|
||||||
|
<if test="message.maxRetryCount != null">
|
||||||
|
max_retry_count = #{message.maxRetryCount},
|
||||||
|
</if>
|
||||||
|
<if test="message.lastError != null">
|
||||||
|
last_error = #{message.lastError},
|
||||||
|
</if>
|
||||||
|
<if test="message.updater != null and message.updater != ''">
|
||||||
|
updater = #{message.updater},
|
||||||
|
</if>
|
||||||
|
<if test="message.updateTime != null">
|
||||||
|
update_time = #{message.updateTime},
|
||||||
|
</if>
|
||||||
|
</set>
|
||||||
|
WHERE message_id = #{message.messageID}
|
||||||
|
AND deleted = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 删除消息(逻辑删除) -->
|
||||||
|
<update id="deleteMessage">
|
||||||
|
UPDATE tb_sys_message
|
||||||
|
SET deleted = 1,
|
||||||
|
delete_time = NOW(),
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE message_id = #{messageID}
|
||||||
|
AND deleted = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 统计消息总数(带权限过滤) -->
|
||||||
|
<select id="countMessage" resultType="java.lang.Integer">
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM tb_sys_message m
|
||||||
|
WHERE m.deleted = 0
|
||||||
|
<if test="filter != null">
|
||||||
|
<if test="filter.title != null and filter.title != ''">
|
||||||
|
AND m.title LIKE CONCAT('%', #{filter.title}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="filter.messageType != null and filter.messageType != ''">
|
||||||
|
AND m.message_type = #{filter.messageType}
|
||||||
|
</if>
|
||||||
|
<if test="filter.status != null and filter.status != ''">
|
||||||
|
AND m.status = #{filter.status}
|
||||||
|
</if>
|
||||||
|
<if test="filter.sendMode != null and filter.sendMode != ''">
|
||||||
|
AND m.send_mode = #{filter.sendMode}
|
||||||
|
</if>
|
||||||
|
<if test="filter.priority != null and filter.priority != ''">
|
||||||
|
AND m.priority = #{filter.priority}
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<!-- 权限过滤:只能查看自己部门及子部门的消息 -->
|
||||||
|
AND (
|
||||||
|
m.sender_dept_id = #{currentUserDeptID}
|
||||||
|
OR m.sender_dept_id IN (
|
||||||
|
WITH RECURSIVE dept_tree AS (
|
||||||
|
SELECT dept_id FROM tb_sys_dept
|
||||||
|
WHERE dept_id = #{currentUserDeptID} AND deleted = 0
|
||||||
|
UNION ALL
|
||||||
|
SELECT d.dept_id FROM tb_sys_dept d
|
||||||
|
INNER JOIN dept_tree dt ON d.parent_id = dt.dept_id
|
||||||
|
WHERE d.deleted = 0
|
||||||
|
)
|
||||||
|
SELECT dept_id FROM dept_tree
|
||||||
|
)
|
||||||
|
)
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?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.message.mapper.MessageTargetMapper">
|
||||||
|
|
||||||
|
<!-- Result Map -->
|
||||||
|
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.message.TbSysMessageTarget">
|
||||||
|
<id column="id" property="id" jdbcType="VARCHAR"/>
|
||||||
|
<result column="message_id" property="messageID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="send_method" property="sendMethod" jdbcType="VARCHAR"/>
|
||||||
|
<result column="target_type" property="targetType" jdbcType="VARCHAR"/>
|
||||||
|
<result column="target_id" property="targetID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="target_name" property="targetName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="scope_dept_id" property="scopeDeptID" jdbcType="VARCHAR"/>
|
||||||
|
<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="BOOLEAN"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<!-- 根据消息ID查询接收对象列表 -->
|
||||||
|
<select id="selectByMessageID" resultMap="BaseResultMap">
|
||||||
|
SELECT *
|
||||||
|
FROM tb_sys_message_target
|
||||||
|
WHERE message_id = #{messageID}
|
||||||
|
AND deleted = 0
|
||||||
|
ORDER BY create_time ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 批量插入接收对象 -->
|
||||||
|
<insert id="batchInsert">
|
||||||
|
INSERT INTO tb_sys_message_target
|
||||||
|
(id, message_id, send_method, target_type, target_id, target_name, scope_dept_id, creator, create_time, update_time, deleted)
|
||||||
|
VALUES
|
||||||
|
<foreach collection="targets" item="item" separator=",">
|
||||||
|
(#{item.id}, #{item.messageID}, #{item.sendMethod}, #{item.targetType}, #{item.targetID},
|
||||||
|
#{item.targetName}, #{item.scopeDeptID}, #{item.creator}, NOW(), NOW(), 0)
|
||||||
|
</foreach>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 根据消息ID删除接收对象 -->
|
||||||
|
<update id="deleteByMessageID">
|
||||||
|
UPDATE tb_sys_message_target
|
||||||
|
SET deleted = 1,
|
||||||
|
delete_time = NOW(),
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE message_id = #{messageID}
|
||||||
|
AND deleted = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
<?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.message.mapper.MessageUserMapper">
|
||||||
|
|
||||||
|
<!-- Result Map -->
|
||||||
|
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.message.TbSysMessageUser">
|
||||||
|
<id column="id" property="id" jdbcType="VARCHAR"/>
|
||||||
|
<result column="message_id" property="messageID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="user_id" property="userID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="send_method" property="sendMethod" jdbcType="VARCHAR"/>
|
||||||
|
<result column="is_read" property="isRead" jdbcType="BOOLEAN"/>
|
||||||
|
<result column="read_time" property="readTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="send_status" property="sendStatus" jdbcType="VARCHAR"/>
|
||||||
|
<result column="fail_reason" property="failReason" jdbcType="VARCHAR"/>
|
||||||
|
<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="BOOLEAN"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<resultMap id="MessageUserVOMap" type="org.xyzh.common.dto.message.MessageUserVO">
|
||||||
|
<id column="id" property="id" jdbcType="VARCHAR"/>
|
||||||
|
<result column="message_id" property="messageID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="user_id" property="userID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="username" property="username" jdbcType="VARCHAR"/>
|
||||||
|
<result column="full_name" property="fullName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="dept_id" property="deptID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="name" property="deptName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="send_method" property="sendMethod" jdbcType="VARCHAR"/>
|
||||||
|
<result column="is_read" property="isRead" jdbcType="BOOLEAN"/>
|
||||||
|
<result column="read_time" property="readTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="send_status" property="sendStatus" jdbcType="VARCHAR"/>
|
||||||
|
<result column="fail_reason" property="failReason" jdbcType="VARCHAR"/>
|
||||||
|
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="title" property="title" jdbcType="VARCHAR"/>
|
||||||
|
<result column="content" property="content" jdbcType="VARCHAR"/>
|
||||||
|
<result column="message_type" property="messageType" jdbcType="VARCHAR"/>
|
||||||
|
<result column="priority" property="priority" jdbcType="VARCHAR"/>
|
||||||
|
<result column="sender_name" property="senderName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="sender_dept_name" property="senderDeptName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="actual_send_time" property="actualSendTime" jdbcType="TIMESTAMP"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<!-- 根据消息ID查询用户消息列表 -->
|
||||||
|
<select id="selectByMessageID" resultMap="MessageUserVOMap">
|
||||||
|
SELECT
|
||||||
|
mu.*,
|
||||||
|
u.username,
|
||||||
|
ui.full_name as full_name,
|
||||||
|
d.dept_id as dept_id,
|
||||||
|
d.name as deptName
|
||||||
|
FROM tb_sys_message_user mu
|
||||||
|
LEFT JOIN tb_sys_user u ON mu.user_id = u.id
|
||||||
|
LEFT JOIN tb_sys_user_info ui ON u.id = ui.user_id
|
||||||
|
LEFT JOIN tb_sys_user_dept_role udr ON u.id = udr.user_id AND udr.deleted = 0
|
||||||
|
LEFT JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0
|
||||||
|
WHERE mu.message_id = #{messageID}
|
||||||
|
AND mu.deleted = 0
|
||||||
|
ORDER BY mu.create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 批量插入用户消息 -->
|
||||||
|
<insert id="batchInsert">
|
||||||
|
INSERT INTO tb_sys_message_user
|
||||||
|
(id, message_id, user_id, send_method, is_read, send_status, creator, create_time, update_time, deleted)
|
||||||
|
VALUES
|
||||||
|
<foreach collection="userMessages" item="item" separator=",">
|
||||||
|
(#{item.id}, #{item.messageID}, #{item.userID}, #{item.sendMethod},
|
||||||
|
0, 'pending', #{item.creator}, NOW(), NOW(), 0)
|
||||||
|
</foreach>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 分页查询当前用户的消息列表 -->
|
||||||
|
<select id="selectMyMessages" resultMap="MessageUserVOMap">
|
||||||
|
SELECT
|
||||||
|
mu.*,
|
||||||
|
m.title,
|
||||||
|
m.content,
|
||||||
|
m.message_type as message_type,
|
||||||
|
m.priority,
|
||||||
|
m.sender_name as sender_name,
|
||||||
|
m.sender_dept_name as sender_dept_name,
|
||||||
|
m.actual_send_time as actual_send_time
|
||||||
|
FROM tb_sys_message_user mu
|
||||||
|
INNER JOIN tb_sys_message m ON mu.message_id = m.message_id
|
||||||
|
WHERE mu.user_id = #{userID}
|
||||||
|
AND mu.deleted = 0
|
||||||
|
AND m.deleted = 0
|
||||||
|
<if test="filter != null">
|
||||||
|
<if test="filter.isRead != null">
|
||||||
|
AND mu.is_read = #{filter.isRead}
|
||||||
|
</if>
|
||||||
|
<if test="filter.sendMethod != null and filter.sendMethod != ''">
|
||||||
|
AND mu.send_method = #{filter.sendMethod}
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
ORDER BY m.actual_send_time DESC, mu.create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询当前用户的消息详情 -->
|
||||||
|
<select id="selectMyMessageDetail" resultMap="MessageUserVOMap">
|
||||||
|
SELECT
|
||||||
|
mu.*,
|
||||||
|
m.title,
|
||||||
|
m.content,
|
||||||
|
m.message_type as message_type,
|
||||||
|
m.priority,
|
||||||
|
m.sender_name as sender_name,
|
||||||
|
m.sender_dept_name as sender_dept_name,
|
||||||
|
m.actual_send_time as actual_send_time
|
||||||
|
FROM tb_sys_message_user mu
|
||||||
|
INNER JOIN tb_sys_message m ON mu.message_id = m.message_id
|
||||||
|
WHERE mu.user_id = #{userID}
|
||||||
|
AND mu.message_id = #{messageID}
|
||||||
|
AND mu.deleted = 0
|
||||||
|
AND m.deleted = 0
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 标记消息为已读 -->
|
||||||
|
<update id="markAsRead">
|
||||||
|
UPDATE tb_sys_message_user
|
||||||
|
SET is_read = 1,
|
||||||
|
send_status = 'sent',
|
||||||
|
read_time = NOW(),
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE user_id = #{userID}
|
||||||
|
AND message_id = #{messageID}
|
||||||
|
AND deleted = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 批量标记消息为已读 -->
|
||||||
|
<update id="batchMarkAsRead">
|
||||||
|
UPDATE tb_sys_message_user
|
||||||
|
SET is_read = 1,
|
||||||
|
read_time = NOW(),
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE user_id = #{userID}
|
||||||
|
AND message_id IN
|
||||||
|
<foreach collection="messageIDs" item="id" open="(" close=")" separator=",">
|
||||||
|
#{id}
|
||||||
|
</foreach>
|
||||||
|
AND deleted = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 查询未读消息数量 -->
|
||||||
|
<select id="countUnread" resultType="java.lang.Integer">
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM tb_sys_message_user
|
||||||
|
WHERE user_id = #{userID}
|
||||||
|
AND is_read = 0
|
||||||
|
AND deleted = 0
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 动态计算未读消息数量(基于 target 配置) -->
|
||||||
|
<select id="countUnreadWithDynamicTargets" resultType="java.lang.Integer">
|
||||||
|
SELECT COUNT(DISTINCT m.message_id)
|
||||||
|
FROM tb_sys_message m
|
||||||
|
INNER JOIN tb_sys_message_target mt ON m.message_id = mt.message_id AND mt.deleted = 0
|
||||||
|
LEFT JOIN tb_sys_message_user mu ON m.message_id = mu.message_id AND mu.user_id = #{userID} AND mu.deleted = 0
|
||||||
|
WHERE m.deleted = 0
|
||||||
|
AND m.status IN ('sent', 'sending', 'failed')
|
||||||
|
AND COALESCE(mu.is_read, 0) = 0
|
||||||
|
AND (
|
||||||
|
-- 用户类型目标:直接匹配
|
||||||
|
(mt.target_type = 'user' AND mt.target_id = #{userID})
|
||||||
|
OR
|
||||||
|
-- 部门类型目标:用户所在部门是目标部门或其子部门
|
||||||
|
(mt.target_type = 'dept' AND EXISTS (
|
||||||
|
SELECT 1 FROM tb_sys_user_dept_role udr
|
||||||
|
INNER JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0
|
||||||
|
INNER JOIN tb_sys_dept target_dept ON target_dept.dept_id = mt.target_id AND target_dept.deleted = 0
|
||||||
|
WHERE udr.user_id = #{userID}
|
||||||
|
AND udr.deleted = 0
|
||||||
|
AND (
|
||||||
|
d.dept_id = target_dept.dept_id
|
||||||
|
OR d.dept_path LIKE CONCAT(target_dept.dept_path, '%')
|
||||||
|
)
|
||||||
|
))
|
||||||
|
OR
|
||||||
|
-- 角色类型目标:用户在指定部门范围内拥有该角色
|
||||||
|
(mt.target_type = 'role' AND EXISTS (
|
||||||
|
SELECT 1 FROM tb_sys_user_dept_role udr
|
||||||
|
INNER JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0
|
||||||
|
WHERE udr.user_id = #{userID}
|
||||||
|
AND udr.deleted = 0
|
||||||
|
AND udr.role_id = mt.target_id
|
||||||
|
AND d.dept_path LIKE CONCAT(
|
||||||
|
(SELECT dept_path FROM tb_sys_dept WHERE dept_id = mt.scope_dept_id AND deleted = 0),
|
||||||
|
'%'
|
||||||
|
)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 更新用户消息的发送状态 -->
|
||||||
|
<update id="updateSendStatus">
|
||||||
|
UPDATE tb_sys_message_user
|
||||||
|
SET send_status = #{sendStatus},
|
||||||
|
<if test="failReason != null and failReason != ''">
|
||||||
|
fail_reason = #{failReason},
|
||||||
|
</if>
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE id = #{id}
|
||||||
|
AND deleted = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 查询待发送的用户消息列表 -->
|
||||||
|
<select id="selectPendingUserMessages" resultMap="BaseResultMap">
|
||||||
|
SELECT *
|
||||||
|
FROM tb_sys_message_user
|
||||||
|
WHERE deleted = 0
|
||||||
|
<if test="filter != null">
|
||||||
|
<if test="filter.messageID != null and filter.messageID != ''">
|
||||||
|
AND message_id = #{filter.messageID}
|
||||||
|
</if>
|
||||||
|
<if test="filter.sendStatus != null and filter.sendStatus != ''">
|
||||||
|
AND send_status = #{filter.sendStatus}
|
||||||
|
</if>
|
||||||
|
<if test="filter.deleted != null">
|
||||||
|
AND deleted = #{filter.deleted}
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
ORDER BY create_time ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 动态查询当前用户可见的消息列表(基于 target 配置计算) -->
|
||||||
|
<select id="selectMyMessagesWithDynamicTargets" resultMap="MessageUserVOMap">
|
||||||
|
SELECT
|
||||||
|
m.message_id,
|
||||||
|
m.title,
|
||||||
|
m.content,
|
||||||
|
m.message_type,
|
||||||
|
m.priority,
|
||||||
|
m.sender_name,
|
||||||
|
m.sender_dept_name,
|
||||||
|
m.actual_send_time,
|
||||||
|
MAX(COALESCE(mu.is_read, 0)) as is_read,
|
||||||
|
MAX(mu.read_time) as read_time,
|
||||||
|
MAX(COALESCE(mu.send_status, 'pending')) as send_status,
|
||||||
|
MAX(mu.id) as id,
|
||||||
|
MAX(mu.user_id) as user_id,
|
||||||
|
MAX(mu.send_method) as send_method,
|
||||||
|
MAX(mu.fail_reason) as fail_reason,
|
||||||
|
MAX(mu.create_time) as create_time
|
||||||
|
FROM tb_sys_message m
|
||||||
|
INNER JOIN tb_sys_message_target mt ON m.message_id = mt.message_id AND mt.deleted = 0
|
||||||
|
LEFT JOIN tb_sys_message_user mu ON m.message_id = mu.message_id AND mu.user_id = #{userID} AND mu.deleted = 0
|
||||||
|
WHERE m.deleted = 0
|
||||||
|
AND m.status IN ('sent', 'sending', 'failed')
|
||||||
|
AND (
|
||||||
|
-- 用户类型目标:直接匹配
|
||||||
|
(mt.target_type = 'user' AND mt.target_id = #{userID})
|
||||||
|
OR
|
||||||
|
-- 部门类型目标:用户所在部门是目标部门或其子部门
|
||||||
|
(mt.target_type = 'dept' AND EXISTS (
|
||||||
|
SELECT 1 FROM tb_sys_user_dept_role udr
|
||||||
|
INNER JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0
|
||||||
|
INNER JOIN tb_sys_dept target_dept ON target_dept.dept_id = mt.target_id AND target_dept.deleted = 0
|
||||||
|
WHERE udr.user_id = #{userID}
|
||||||
|
AND udr.deleted = 0
|
||||||
|
AND (
|
||||||
|
d.dept_id = target_dept.dept_id
|
||||||
|
OR d.dept_path LIKE CONCAT(target_dept.dept_path, '%')
|
||||||
|
)
|
||||||
|
))
|
||||||
|
OR
|
||||||
|
-- 角色类型目标:用户在指定部门范围内拥有该角色
|
||||||
|
(mt.target_type = 'role' AND EXISTS (
|
||||||
|
SELECT 1 FROM tb_sys_user_dept_role udr
|
||||||
|
INNER JOIN tb_sys_dept d ON udr.dept_id = d.dept_id AND d.deleted = 0
|
||||||
|
WHERE udr.user_id = #{userID}
|
||||||
|
AND udr.deleted = 0
|
||||||
|
AND udr.role_id = mt.target_id
|
||||||
|
AND d.dept_path LIKE CONCAT(
|
||||||
|
(SELECT dept_path FROM tb_sys_dept WHERE dept_id = mt.scope_dept_id AND deleted = 0),
|
||||||
|
'%'
|
||||||
|
)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
<if test="filter != null">
|
||||||
|
<if test="filter.isRead != null">
|
||||||
|
AND COALESCE(mu.is_read, 0) = #{filter.isRead}
|
||||||
|
</if>
|
||||||
|
<if test="filter.sendMethod != null and filter.sendMethod != ''">
|
||||||
|
AND mt.send_method = #{filter.sendMethod}
|
||||||
|
</if>
|
||||||
|
<if test="filter.sendStatus != null and filter.sendStatus != ''">
|
||||||
|
AND mu.send_status = #{filter.sendStatus}
|
||||||
|
</if>
|
||||||
|
<if test="filter.priority != null and filter.priority != ''">
|
||||||
|
AND m.priority = #{filter.priority}
|
||||||
|
</if>
|
||||||
|
<if test="filter.messageType != null and filter.messageType != ''">
|
||||||
|
AND m.message_type = #{filter.messageType}
|
||||||
|
</if>
|
||||||
|
<if test="filter.title != null and filter.title != ''">
|
||||||
|
AND m.title LIKE CONCAT('%', #{filter.title}, '%')
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
GROUP BY m.message_id, m.title, m.content, m.message_type, m.priority,
|
||||||
|
m.sender_name, m.sender_dept_name, m.actual_send_time
|
||||||
|
ORDER BY m.actual_send_time DESC, m.create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询或创建用户消息记录 -->
|
||||||
|
<select id="selectOrCreateUserMessage" resultMap="MessageUserVOMap">
|
||||||
|
SELECT
|
||||||
|
mu.*,
|
||||||
|
m.title,
|
||||||
|
m.content,
|
||||||
|
m.message_type,
|
||||||
|
m.priority,
|
||||||
|
m.sender_name,
|
||||||
|
m.sender_dept_name,
|
||||||
|
m.actual_send_time
|
||||||
|
FROM tb_sys_message m
|
||||||
|
LEFT JOIN tb_sys_message_user mu ON m.message_id = mu.message_id
|
||||||
|
AND mu.user_id = #{userID}
|
||||||
|
AND mu.deleted = 0
|
||||||
|
WHERE m.message_id = #{messageID}
|
||||||
|
AND m.deleted = 0
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 插入用户消息记录(如果不存在) -->
|
||||||
|
<insert id="insertIfNotExists">
|
||||||
|
INSERT INTO tb_sys_message_user
|
||||||
|
(id, message_id, user_id, send_method, is_read, send_status, creator, create_time, update_time, deleted)
|
||||||
|
SELECT
|
||||||
|
#{userMessage.id},
|
||||||
|
#{userMessage.messageID},
|
||||||
|
#{userMessage.userID},
|
||||||
|
#{userMessage.sendMethod},
|
||||||
|
0,
|
||||||
|
'pending',
|
||||||
|
#{userMessage.creator},
|
||||||
|
NOW(),
|
||||||
|
NOW(),
|
||||||
|
0
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM tb_sys_message_user
|
||||||
|
WHERE message_id = #{userMessage.messageID}
|
||||||
|
AND user_id = #{userMessage.userID}
|
||||||
|
AND deleted = 0
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>news</artifactId>
|
<artifactId>news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
@@ -22,22 +22,22 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-news</artifactId>
|
<artifactId>api-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-usercenter</artifactId>
|
<artifactId>api-usercenter</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>system</artifactId>
|
<artifactId>system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-all</artifactId>
|
<artifactId>common-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>api</module>
|
<module>api</module>
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<module>ai</module>
|
<module>ai</module>
|
||||||
<module>file</module>
|
<module>file</module>
|
||||||
<module>crontab</module>
|
<module>crontab</module>
|
||||||
|
<module>message</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
@@ -67,62 +68,67 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api</artifactId>
|
<artifactId>api</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common</artifactId>
|
<artifactId>common</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-all</artifactId>
|
<artifactId>common-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>auth</artifactId>
|
<artifactId>auth</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>system</artifactId>
|
<artifactId>system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>news</artifactId>
|
<artifactId>news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>course</artifactId>
|
<artifactId>course</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>study</artifactId>
|
<artifactId>study</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>usercenter</artifactId>
|
<artifactId>usercenter</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>achievement</artifactId>
|
<artifactId>achievement</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>ai</artifactId>
|
<artifactId>ai</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>crontab</artifactId>
|
<artifactId>crontab</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xyzh</groupId>
|
||||||
|
<artifactId>message</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- Spring相关依赖 -->
|
<!-- Spring相关依赖 -->
|
||||||
<!-- 覆盖SpringFramework的依赖配置-->
|
<!-- 覆盖SpringFramework的依赖配置-->
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
-- 为 tb_data_collection_item 表添加单条新闻执行状态和消息字段
|
|
||||||
-- 执行日期: 2025-11-12
|
|
||||||
|
|
||||||
ALTER TABLE tb_data_collection_item
|
|
||||||
ADD COLUMN execute_status INT DEFAULT 1 COMMENT '单条新闻执行状态(0:失败 1:成功)' AFTER processor,
|
|
||||||
ADD COLUMN execute_message VARCHAR(500) DEFAULT NULL COMMENT '单条新闻执行消息(记录错误信息或成功提示)' AFTER execute_status;
|
|
||||||
|
|
||||||
-- 为现有数据设置默认值
|
|
||||||
UPDATE tb_data_collection_item SET execute_status = 1 WHERE execute_status IS NULL;
|
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>study</artifactId>
|
<artifactId>study</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>study</name>
|
<name>study</name>
|
||||||
<description>学习管理模块</description>
|
<description>学习管理模块</description>
|
||||||
@@ -25,17 +25,17 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-study</artifactId>
|
<artifactId>api-study</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>system</artifactId>
|
<artifactId>system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-all</artifactId>
|
<artifactId>common-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>system</artifactId>
|
<artifactId>system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
@@ -23,14 +23,14 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-system</artifactId>
|
<artifactId>api-system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Common模块依赖 -->
|
<!-- Common模块依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-all</artifactId>
|
<artifactId>common-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Spring Boot Web -->
|
<!-- Spring Boot Web -->
|
||||||
|
|||||||
@@ -50,6 +50,15 @@ public interface UserMapper extends BaseMapper<TbSysUser> {
|
|||||||
*/
|
*/
|
||||||
int deleteUser(@Param("userID") String userID);
|
int deleteUser(@Param("userID") String userID);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 根据用户ID查询用户
|
||||||
|
* @param userID 用户ID
|
||||||
|
* @return TbSysUser 用户信息
|
||||||
|
* @author yslg
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
TbSysUser selectUserById(@Param("userID") String userID);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 根据用户名查询用户
|
* @description 根据用户名查询用户
|
||||||
* @param username 用户名
|
* @param username 用户名
|
||||||
@@ -184,4 +193,19 @@ public interface UserMapper extends BaseMapper<TbSysUser> {
|
|||||||
|
|
||||||
|
|
||||||
int countDeptUser(@Param("deptId") String deptId);
|
int countDeptUser(@Param("deptId") String deptId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 查询部门及其子部门的所有用户ID
|
||||||
|
* @param deptId 部门ID
|
||||||
|
* @return List<String> 用户ID列表
|
||||||
|
*/
|
||||||
|
List<String> selectUserIdsByDeptId(@Param("deptId") String deptId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 查询指定部门及其子部门中指定角色的所有用户ID
|
||||||
|
* @param deptId 部门ID
|
||||||
|
* @param roleId 角色ID
|
||||||
|
* @return List<String> 用户ID列表
|
||||||
|
*/
|
||||||
|
List<String> selectUserIdsByDeptRole(@Param("deptId") String deptId, @Param("roleId") String roleId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,15 @@
|
|||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<!-- 根据用户名查询用户 -->
|
<!-- 根据用户名查询用户 -->
|
||||||
|
<!-- 根据用户ID查询用户 -->
|
||||||
|
<select id="selectUserById" resultMap="BaseResultMap">
|
||||||
|
SELECT
|
||||||
|
<include refid="Base_Column_List"/>
|
||||||
|
FROM tb_sys_user
|
||||||
|
WHERE id = #{userID}
|
||||||
|
AND deleted = 0
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectByUsername" resultMap="BaseResultMap">
|
<select id="selectByUsername" resultMap="BaseResultMap">
|
||||||
SELECT
|
SELECT
|
||||||
<include refid="Base_Column_List"/>
|
<include refid="Base_Column_List"/>
|
||||||
@@ -573,6 +582,35 @@
|
|||||||
)
|
)
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- selectUserIdsByDeptId - 查询部门及其子部门的所有用户ID -->
|
||||||
|
<select id="selectUserIdsByDeptId" resultType="java.lang.String">
|
||||||
|
SELECT DISTINCT tudr.user_id
|
||||||
|
FROM tb_sys_user_dept_role tudr
|
||||||
|
INNER JOIN tb_sys_dept d ON tudr.dept_id = d.dept_id AND d.deleted = 0
|
||||||
|
INNER JOIN tb_sys_user u ON tudr.user_id = u.id AND u.deleted = 0
|
||||||
|
WHERE tudr.deleted = 0
|
||||||
|
AND u.status = 1
|
||||||
|
AND d.dept_path LIKE CONCAT(
|
||||||
|
(SELECT dept_path FROM tb_sys_dept WHERE dept_id = #{deptId} AND deleted = 0),
|
||||||
|
'%'
|
||||||
|
)
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- selectUserIdsByDeptRole - 查询指定部门及其子部门中指定角色的所有用户ID -->
|
||||||
|
<select id="selectUserIdsByDeptRole" resultType="java.lang.String">
|
||||||
|
SELECT DISTINCT tudr.user_id
|
||||||
|
FROM tb_sys_user_dept_role tudr
|
||||||
|
INNER JOIN tb_sys_dept d ON tudr.dept_id = d.dept_id AND d.deleted = 0
|
||||||
|
INNER JOIN tb_sys_user u ON tudr.user_id = u.id AND u.deleted = 0
|
||||||
|
WHERE tudr.deleted = 0
|
||||||
|
AND u.status = 1
|
||||||
|
AND tudr.role_id = #{roleId}
|
||||||
|
AND d.dept_path LIKE CONCAT(
|
||||||
|
(SELECT dept_path FROM tb_sys_dept WHERE dept_id = #{deptId} AND deleted = 0),
|
||||||
|
'%'
|
||||||
|
)
|
||||||
|
</select>
|
||||||
|
|
||||||
<!-- selectLoginUser -->
|
<!-- selectLoginUser -->
|
||||||
|
|
||||||
<select id="selectLoginUser">
|
<select id="selectLoginUser">
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>school-news</artifactId>
|
<artifactId>school-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>usercenter</artifactId>
|
<artifactId>usercenter</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>usercenter</name>
|
<name>usercenter</name>
|
||||||
<description>个人中心模块</description>
|
<description>个人中心模块</description>
|
||||||
@@ -25,33 +25,33 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-usercenter</artifactId>
|
<artifactId>api-usercenter</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-system</artifactId>
|
<artifactId>api-system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-news</artifactId>
|
<artifactId>api-news</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-study</artifactId>
|
<artifactId>api-study</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>common-all</artifactId>
|
<artifactId>common-all</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>system</artifactId>
|
<artifactId>system</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.xyzh</groupId>
|
<groupId>org.xyzh</groupId>
|
||||||
<artifactId>api-file</artifactId>
|
<artifactId>api-file</artifactId>
|
||||||
<version>${school-news.version}</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const aiAgentConfigApi = {
|
|||||||
* @returns Promise<ResultDomain<AiAgentConfig[]>>
|
* @returns Promise<ResultDomain<AiAgentConfig[]>>
|
||||||
*/
|
*/
|
||||||
async listEnabledAgents(): Promise<ResultDomain<AiAgentConfig>> {
|
async listEnabledAgents(): Promise<ResultDomain<AiAgentConfig>> {
|
||||||
const response = await api.get<AiAgentConfig>('/ai/agent/enabled', {
|
const response = await api.get<AiAgentConfig>('/ai/agent/enabled',{}, {
|
||||||
showLoading: false
|
showLoading: false
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ request.interceptors.request.use(
|
|||||||
(config: InternalAxiosRequestConfig) => {
|
(config: InternalAxiosRequestConfig) => {
|
||||||
const customConfig = config as CustomAxiosRequestConfig;
|
const customConfig = config as CustomAxiosRequestConfig;
|
||||||
|
|
||||||
// 显示加载动画
|
// 默认不显示加载动画,只有显式开启时才展示
|
||||||
if (customConfig.showLoading !== false) {
|
if (customConfig.showLoading) {
|
||||||
loadingInstance = ElLoading.service({
|
loadingInstance = ElLoading.service({
|
||||||
lock: true,
|
lock: true,
|
||||||
text: "加载中...",
|
text: "加载中...",
|
||||||
|
|||||||
7
schoolNewsWeb/src/apis/message/index.ts
Normal file
7
schoolNewsWeb/src/apis/message/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @description 消息通知API导出
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './message';
|
||||||
216
schoolNewsWeb/src/apis/message/message.ts
Normal file
216
schoolNewsWeb/src/apis/message/message.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* @description 消息通知相关API
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { api } from '@/apis/index';
|
||||||
|
import type {
|
||||||
|
TbSysMessage,
|
||||||
|
TbSysMessageUser,
|
||||||
|
MessageVO,
|
||||||
|
MessageUserVO,
|
||||||
|
ResultDomain,
|
||||||
|
PageParam,
|
||||||
|
PageRequest
|
||||||
|
} from '@/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息API服务
|
||||||
|
*/
|
||||||
|
export const messageApi = {
|
||||||
|
/**
|
||||||
|
* @description 创建消息
|
||||||
|
* @param message 消息对象
|
||||||
|
* @returns Promise<ResultDomain<TbSysMessage>>
|
||||||
|
*/
|
||||||
|
async createMessage(message: TbSysMessage): Promise<ResultDomain<TbSysMessage>> {
|
||||||
|
const response = await api.post<TbSysMessage>('/message', message);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 更新消息
|
||||||
|
* @param messageIDOrMessage 消息ID 或 完整消息对象
|
||||||
|
* @param messageData 消息数据(当第一个参数为ID时使用)
|
||||||
|
* @returns Promise<ResultDomain<TbSysMessage>>
|
||||||
|
*/
|
||||||
|
async updateMessage(message: TbSysMessage): Promise<ResultDomain<TbSysMessage>> {
|
||||||
|
const response = await api.put<TbSysMessage>(`/message`, message);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 删除消息
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @returns Promise<ResultDomain<boolean>>
|
||||||
|
*/
|
||||||
|
async deleteMessage(messageID: string): Promise<ResultDomain<boolean>> {
|
||||||
|
const response = await api.delete<boolean>(`/message/${messageID}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取消息分页列表(管理端)
|
||||||
|
* @param pageRequest 分页请求对象
|
||||||
|
* @returns Promise<ResultDomain<MessageVO>>
|
||||||
|
*/
|
||||||
|
async getMessagePage(pageRequest: PageRequest<TbSysMessage>): Promise<ResultDomain<MessageVO>> {
|
||||||
|
const response = await api.post<MessageVO>('/message/page', pageRequest);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取消息详情(管理端)
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @returns Promise<ResultDomain<MessageVO>>
|
||||||
|
*/
|
||||||
|
async getMessage(messageID: string): Promise<ResultDomain<MessageVO>> {
|
||||||
|
return this.getMessageDetail(messageID);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取消息详情(管理端)
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @returns Promise<ResultDomain<MessageVO>>
|
||||||
|
*/
|
||||||
|
async getMessageDetail(messageID: string): Promise<ResultDomain<MessageVO>> {
|
||||||
|
const response = await api.get<MessageVO>(`/message/detail/${messageID}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 立即发送消息
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @returns Promise<ResultDomain<TbSysMessage>>
|
||||||
|
*/
|
||||||
|
async sendMessage(messageID: string): Promise<ResultDomain<TbSysMessage>> {
|
||||||
|
const response = await api.post<TbSysMessage>(`/message/send/${messageID}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 立即发送(将定时消息改为立即发送)
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @returns Promise<ResultDomain<TbSysMessage>>
|
||||||
|
*/
|
||||||
|
async sendNow(messageID: string): Promise<ResultDomain<TbSysMessage>> {
|
||||||
|
const response = await api.post<TbSysMessage>(`/message/sendNow/${messageID}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 取消定时消息
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @returns Promise<ResultDomain<TbSysMessage>>
|
||||||
|
*/
|
||||||
|
async cancelMessage(messageID: string): Promise<ResultDomain<TbSysMessage>> {
|
||||||
|
const response = await api.post<TbSysMessage>(`/message/cancel/${messageID}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 重新安排定时消息时间
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @param newScheduledTime 新的定时发送时间
|
||||||
|
* @returns Promise<ResultDomain<TbSysMessage>>
|
||||||
|
*/
|
||||||
|
async rescheduleMessage(messageID: string, newScheduledTime: string): Promise<ResultDomain<TbSysMessage>> {
|
||||||
|
const response = await api.put<TbSysMessage>(`/message/reschedule/${messageID}`, {
|
||||||
|
scheduledTime: newScheduledTime
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 重试发送失败的消息
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @returns Promise<ResultDomain<TbSysMessage>>
|
||||||
|
*/
|
||||||
|
async retryMessage(messageID: string): Promise<ResultDomain<TbSysMessage>> {
|
||||||
|
const response = await api.post<TbSysMessage>(`/message/retry/${messageID}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取当前用户的消息列表(用户端)
|
||||||
|
* @param pageParam 分页参数
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @returns Promise<ResultDomain<MessageUserVO>>
|
||||||
|
*/
|
||||||
|
async getMyMessages(pageParam: PageParam, filter?: TbSysMessageUser): Promise<ResultDomain<MessageUserVO>> {
|
||||||
|
const response = await api.post<MessageUserVO>('/message/my/page', {
|
||||||
|
pageParam,
|
||||||
|
filter
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取当前用户的消息详情(用户端)
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @returns Promise<ResultDomain<MessageUserVO>>
|
||||||
|
*/
|
||||||
|
async getMyMessageDetail(messageID: string): Promise<ResultDomain<MessageUserVO>> {
|
||||||
|
const response = await api.get<MessageUserVO>(`/message/my/detail/${messageID}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 标记消息为已读
|
||||||
|
* @param messageID 消息ID
|
||||||
|
* @returns Promise<ResultDomain<boolean>>
|
||||||
|
*/
|
||||||
|
async markAsRead(messageID: string): Promise<ResultDomain<boolean>> {
|
||||||
|
const response = await api.post<boolean>(`/message/my/markRead/${messageID}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 批量标记消息为已读
|
||||||
|
* @param messageIDs 消息ID列表
|
||||||
|
* @returns Promise<ResultDomain<boolean>>
|
||||||
|
*/
|
||||||
|
async batchMarkAsRead(messageIDs: string[]): Promise<ResultDomain<boolean>> {
|
||||||
|
const response = await api.post<boolean>('/message/my/batchMarkRead', { messageIDs });
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取未读消息数量
|
||||||
|
* @returns Promise<ResultDomain<number>>
|
||||||
|
*/
|
||||||
|
async getUnreadCount(): Promise<ResultDomain<number>> {
|
||||||
|
const response = await api.get<number>('/message/my/unreadCount');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取可选择的部门列表(当前部门及子部门)
|
||||||
|
* @returns Promise<ResultDomain<any>>
|
||||||
|
*/
|
||||||
|
async getTargetDepts(): Promise<ResultDomain<any>> {
|
||||||
|
const response = await api.get<any>('/message/targets/depts');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取可选择的角色列表(指定部门范围内的角色)
|
||||||
|
* @param scopeDeptID 作用域部门ID
|
||||||
|
* @returns Promise<ResultDomain<any>>
|
||||||
|
*/
|
||||||
|
async getTargetRoles(scopeDeptID: string): Promise<ResultDomain<any>> {
|
||||||
|
const response = await api.get<any>(`/message/targets/roles?scopeDeptID=${scopeDeptID}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取可选择的用户列表(指定部门范围内的用户)
|
||||||
|
* @param scopeDeptID 作用域部门ID
|
||||||
|
* @returns Promise<ResultDomain<any>>
|
||||||
|
*/
|
||||||
|
async getTargetUsers(scopeDeptID: string): Promise<ResultDomain<any>> {
|
||||||
|
const response = await api.get<any>(`/message/targets/users?scopeDeptID=${scopeDeptID}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<span class="message-priority-badge" :class="`priority-${priority}`">
|
||||||
|
{{ priorityText }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 优先级:urgent-紧急/important-重要/normal-普通 */
|
||||||
|
priority: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
/** 优先级文本映射 */
|
||||||
|
const priorityText = computed(() => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
urgent: '紧急',
|
||||||
|
important: '重要',
|
||||||
|
normal: '普通'
|
||||||
|
};
|
||||||
|
return map[props.priority] || props.priority;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-priority-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&.priority-urgent {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #c00;
|
||||||
|
border: 1px solid #fcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.priority-important {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #f57c00;
|
||||||
|
border: 1px solid #ffcc80;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.priority-normal {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
border: 1px solid #a5d6a7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<div class="message-send-method-selector">
|
||||||
|
<div class="method-option">
|
||||||
|
<label class="method-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value="system"
|
||||||
|
:checked="selectedMethods.includes('system')"
|
||||||
|
@change="toggleMethod('system')"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
<span class="method-text">
|
||||||
|
<i class="el-icon-message"></i>
|
||||||
|
系统消息
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<span class="method-desc">在系统内推送消息通知</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method-option">
|
||||||
|
<label class="method-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value="email"
|
||||||
|
:checked="selectedMethods.includes('email')"
|
||||||
|
@change="toggleMethod('email')"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
<span class="method-text">
|
||||||
|
<i class="el-icon-message-solid"></i>
|
||||||
|
邮件通知
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<span class="method-desc">发送到用户邮箱</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method-option">
|
||||||
|
<label class="method-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value="sms"
|
||||||
|
:checked="selectedMethods.includes('sms')"
|
||||||
|
@change="toggleMethod('sms')"
|
||||||
|
:disabled="disabled"
|
||||||
|
/>
|
||||||
|
<span class="method-text">
|
||||||
|
<i class="el-icon-phone"></i>
|
||||||
|
短信通知
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<span class="method-desc">发送到用户手机</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error-tip">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 已选择的发送方式列表 */
|
||||||
|
modelValue: string[];
|
||||||
|
/** 是否禁用 */
|
||||||
|
disabled?: boolean;
|
||||||
|
/** 是否必须至少选择一个 */
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: string[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
disabled: false,
|
||||||
|
required: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const selectedMethods = computed(() => props.modelValue);
|
||||||
|
|
||||||
|
const error = computed(() => {
|
||||||
|
if (props.required && selectedMethods.value.length === 0) {
|
||||||
|
return '请至少选择一种发送方式';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 切换发送方式 */
|
||||||
|
function toggleMethod(method: string) {
|
||||||
|
if (props.disabled) return;
|
||||||
|
|
||||||
|
const methods = [...selectedMethods.value];
|
||||||
|
const index = methods.indexOf(method);
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
// 如果是必选且只剩一个,不允许取消
|
||||||
|
if (props.required && methods.length === 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
methods.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
methods.push(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update:modelValue', methods);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-send-method-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.method-option {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-color: #c8232c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-desc {
|
||||||
|
margin-left: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c00;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
76
schoolNewsWeb/src/components/message/MessageStatusBadge.vue
Normal file
76
schoolNewsWeb/src/components/message/MessageStatusBadge.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<span class="message-status-badge" :class="`status-${status}`">
|
||||||
|
{{ statusText }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 消息状态:draft-草稿/pending-待发送/sending-发送中/sent-已发送/failed-发送失败/cancelled-已取消 */
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
/** 状态文本映射 */
|
||||||
|
const statusText = computed(() => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
draft: '草稿',
|
||||||
|
pending: '待发送',
|
||||||
|
sending: '发送中',
|
||||||
|
sent: '已发送',
|
||||||
|
failed: '发送失败',
|
||||||
|
cancelled: '已取消'
|
||||||
|
};
|
||||||
|
return map[props.status] || props.status;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.status-draft {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-pending {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #f57c00;
|
||||||
|
border: 1px solid #ffcc80;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-sending {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
border: 1px solid #90caf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-sent {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
border: 1px solid #a5d6a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-failed {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #c00;
|
||||||
|
border: 1px solid #fcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-cancelled {
|
||||||
|
background-color: #fafafa;
|
||||||
|
color: #999;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
649
schoolNewsWeb/src/components/message/MessageTargetSelector.vue
Normal file
649
schoolNewsWeb/src/components/message/MessageTargetSelector.vue
Normal file
@@ -0,0 +1,649 @@
|
|||||||
|
<template>
|
||||||
|
<div class="message-target-selector">
|
||||||
|
<!-- Tab切换 -->
|
||||||
|
<div class="target-tabs">
|
||||||
|
<div
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: activeTab === tab.key }"
|
||||||
|
@click="switchTab(tab.key)"
|
||||||
|
>
|
||||||
|
<i :class="tab.icon"></i>
|
||||||
|
{{ tab.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab内容 -->
|
||||||
|
<div class="target-content">
|
||||||
|
<!-- 部门选择 -->
|
||||||
|
<div v-if="activeTab === 'dept'" class="target-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>已选择 {{ selectedDepts.length }} 个部门</span>
|
||||||
|
<button class="btn-select" @click="showDeptSelector">
|
||||||
|
<i class="el-icon-plus"></i> 选择部门
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="selected-list">
|
||||||
|
<div
|
||||||
|
v-for="item in selectedDepts"
|
||||||
|
:key="item.id"
|
||||||
|
class="selected-item"
|
||||||
|
>
|
||||||
|
<i class="el-icon-office-building"></i>
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
<i class="el-icon-close remove-btn" @click="removeDept(item.id)"></i>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedDepts.length === 0" class="empty-tip">
|
||||||
|
请选择部门
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 角色选择 -->
|
||||||
|
<div v-if="activeTab === 'role'" class="target-panel">
|
||||||
|
<div class="scope-selector">
|
||||||
|
<label>作用域部门:</label>
|
||||||
|
<select v-model="roleScopeDeptID" class="dept-select" @change="onRoleScopeChange">
|
||||||
|
<option value="">请选择部门范围</option>
|
||||||
|
<option v-for="dept in availableDepts" :key="dept.id" :value="dept.id">
|
||||||
|
{{ dept.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<span class="tip">(限制该角色只能向此部门及子部门的用户发送)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="roleScopeDeptID" class="panel-header">
|
||||||
|
<span>已选择 {{ selectedRoles.length }} 个角色</span>
|
||||||
|
<button class="btn-select" @click="showRoleSelector">
|
||||||
|
<i class="el-icon-plus"></i> 选择角色
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="roleScopeDeptID" class="selected-list">
|
||||||
|
<div
|
||||||
|
v-for="item in selectedRoles"
|
||||||
|
:key="item.id"
|
||||||
|
class="selected-item"
|
||||||
|
>
|
||||||
|
<i class="el-icon-user"></i>
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
<span class="item-sublabel">{{ item.deptName }}</span>
|
||||||
|
<i class="el-icon-close remove-btn" @click="removeRole(item.id)"></i>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedRoles.length === 0" class="empty-tip">
|
||||||
|
请选择角色
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户选择 -->
|
||||||
|
<div v-if="activeTab === 'user'" class="target-panel">
|
||||||
|
<div class="scope-selector">
|
||||||
|
<label>作用域部门:</label>
|
||||||
|
<select v-model="userScopeDeptID" class="dept-select" @change="onUserScopeChange">
|
||||||
|
<option value="">请选择部门范围</option>
|
||||||
|
<option v-for="dept in availableDepts" :key="dept.id" :value="dept.id">
|
||||||
|
{{ dept.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<span class="tip">(只能选择此部门及子部门的用户)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="userScopeDeptID" class="panel-header">
|
||||||
|
<span>已选择 {{ selectedUsers.length }} 个用户</span>
|
||||||
|
<button class="btn-select" @click="showUserSelector">
|
||||||
|
<i class="el-icon-plus"></i> 选择用户
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="userScopeDeptID" class="selected-list">
|
||||||
|
<div
|
||||||
|
v-for="item in selectedUsers"
|
||||||
|
:key="item.id"
|
||||||
|
class="selected-item"
|
||||||
|
>
|
||||||
|
<i class="el-icon-user-solid"></i>
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
<span class="item-sublabel">{{ item.deptName }}</span>
|
||||||
|
<i class="el-icon-close remove-btn" @click="removeUser(item.id)"></i>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedUsers.length === 0" class="empty-tip">
|
||||||
|
请选择用户
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 通用选择器弹窗 -->
|
||||||
|
<GenericSelector
|
||||||
|
v-model:visible="selectorVisible"
|
||||||
|
:title="selectorTitle"
|
||||||
|
:fetch-available-api="fetchAvailableApi"
|
||||||
|
:fetch-selected-api="fetchSelectedApi"
|
||||||
|
:filter-selected="filterSelected"
|
||||||
|
:item-config="itemConfig"
|
||||||
|
:unit-name="unitName"
|
||||||
|
@confirm="handleSelectorConfirm"
|
||||||
|
@cancel="selectorVisible = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
|
import { GenericSelector } from '@/components/base';
|
||||||
|
import type { TbSysMessageTarget, ResultDomain } from '@/types';
|
||||||
|
import { deptApi, roleApi, userApi } from '@/apis/system';
|
||||||
|
|
||||||
|
// 本地类型定义
|
||||||
|
interface TargetOption {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
deptID?: string;
|
||||||
|
deptName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: TbSysMessageTarget[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 已选择的目标配置列表 */
|
||||||
|
modelValue: TbSysMessageTarget[];
|
||||||
|
/** 发送方式(用于生成TbSysMessageTarget) */
|
||||||
|
sendMethod: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
/** Tab配置 */
|
||||||
|
const tabs = [
|
||||||
|
{ key: 'dept', label: '按部门', icon: 'el-icon-office-building' },
|
||||||
|
{ key: 'role', label: '按角色', icon: 'el-icon-user' },
|
||||||
|
{ key: 'user', label: '按用户', icon: 'el-icon-user-solid' }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const activeTab = ref<'dept' | 'role' | 'user'>('dept');
|
||||||
|
|
||||||
|
// 可选的部门列表(当前部门及子部门)
|
||||||
|
const availableDepts = ref<TargetOption[]>([]);
|
||||||
|
|
||||||
|
// 已选择的目标
|
||||||
|
const selectedDepts = ref<TargetOption[]>([]);
|
||||||
|
const selectedRoles = ref<TargetOption[]>([]);
|
||||||
|
const selectedUsers = ref<TargetOption[]>([]);
|
||||||
|
|
||||||
|
// 作用域部门ID
|
||||||
|
const roleScopeDeptID = ref('');
|
||||||
|
const userScopeDeptID = ref('');
|
||||||
|
|
||||||
|
// 选择器状态
|
||||||
|
const selectorVisible = ref(false);
|
||||||
|
const selectorType = ref<'dept' | 'role' | 'user'>('dept');
|
||||||
|
|
||||||
|
/** 选择器标题 */
|
||||||
|
const selectorTitle = computed(() => {
|
||||||
|
const map = {
|
||||||
|
dept: '选择部门',
|
||||||
|
role: '选择角色',
|
||||||
|
user: '选择用户'
|
||||||
|
};
|
||||||
|
return map[selectorType.value];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 选择器单位名称 */
|
||||||
|
const unitName = computed(() => {
|
||||||
|
const map = {
|
||||||
|
dept: '个部门',
|
||||||
|
role: '个角色',
|
||||||
|
user: '个用户'
|
||||||
|
};
|
||||||
|
return map[selectorType.value];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 选择器字段配置 */
|
||||||
|
const itemConfig = computed(() => ({
|
||||||
|
id: 'id',
|
||||||
|
label: 'name',
|
||||||
|
sublabel: selectorType.value !== 'dept' ? 'deptName' : undefined
|
||||||
|
}));
|
||||||
|
|
||||||
|
/** 获取可选项API */
|
||||||
|
const fetchAvailableApi = computed(() => {
|
||||||
|
return async () => {
|
||||||
|
if (selectorType.value === 'dept') {
|
||||||
|
// 调用 system 模块的部门 API
|
||||||
|
const result = await deptApi.getAllDepts();
|
||||||
|
// 转换数据格式为 GenericSelector 需要的格式
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
dataList: result.dataList.map((dept: any) => ({
|
||||||
|
id: dept.id,
|
||||||
|
name: dept.name,
|
||||||
|
deptID: dept.id,
|
||||||
|
deptName: dept.name
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else if (selectorType.value === 'role') {
|
||||||
|
// 调用 system 模块的角色 API,根据部门过滤
|
||||||
|
const filter = roleScopeDeptID.value ? { deptID: roleScopeDeptID.value } : {};
|
||||||
|
const result = await roleApi.getRoleList(filter as any);
|
||||||
|
// 转换数据格式
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
dataList: result.dataList.map((role: any) => ({
|
||||||
|
id: role.id,
|
||||||
|
name: role.name,
|
||||||
|
deptID: role.deptID,
|
||||||
|
deptName: role.deptName
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
// 调用 system 模块的用户 API,根据部门过滤
|
||||||
|
const filter = userScopeDeptID.value ? { deptID: userScopeDeptID.value } : {};
|
||||||
|
const result = await userApi.getUserList(filter as any);
|
||||||
|
// 转换数据格式
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
dataList: result.dataList.map((user: any) => ({
|
||||||
|
id: user.id,
|
||||||
|
name: user.realName || user.username,
|
||||||
|
deptID: user.deptID,
|
||||||
|
deptName: user.deptName
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 获取已选项API(返回当前已选择的项) */
|
||||||
|
const fetchSelectedApi = computed(() => {
|
||||||
|
return async (): Promise<ResultDomain<TargetOption>> => {
|
||||||
|
let dataList: TargetOption[] = [];
|
||||||
|
if (selectorType.value === 'dept') {
|
||||||
|
dataList = selectedDepts.value;
|
||||||
|
} else if (selectorType.value === 'role') {
|
||||||
|
dataList = selectedRoles.value;
|
||||||
|
} else {
|
||||||
|
dataList = selectedUsers.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'success',
|
||||||
|
success: true,
|
||||||
|
login: true,
|
||||||
|
auth: true,
|
||||||
|
dataList
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 过滤已选项(从可选项中移除已选项) */
|
||||||
|
const filterSelected = (available: Record<string, any>[], selected: Record<string, any>[]): Record<string, any>[] => {
|
||||||
|
const availableOptions = available as TargetOption[];
|
||||||
|
const selectedOptions = selected as TargetOption[];
|
||||||
|
const selectedIds = new Set(selectedOptions.map(item => item.id));
|
||||||
|
return availableOptions.filter(item => !selectedIds.has(item.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 加载可用部门列表 */
|
||||||
|
async function loadAvailableDepts() {
|
||||||
|
try {
|
||||||
|
const result = await deptApi.getAllDepts();
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
// 转换数据格式
|
||||||
|
availableDepts.value = result.dataList.map((dept: any) => ({
|
||||||
|
id: dept.id,
|
||||||
|
name: dept.name,
|
||||||
|
deptID: dept.id,
|
||||||
|
deptName: dept.name
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载部门列表失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换Tab */
|
||||||
|
function switchTab(tab: 'dept' | 'role' | 'user') {
|
||||||
|
activeTab.value = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示部门选择器 */
|
||||||
|
function showDeptSelector() {
|
||||||
|
selectorType.value = 'dept';
|
||||||
|
selectorVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示角色选择器 */
|
||||||
|
function showRoleSelector() {
|
||||||
|
if (!roleScopeDeptID.value) {
|
||||||
|
alert('请先选择作用域部门');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectorType.value = 'role';
|
||||||
|
selectorVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显示用户选择器 */
|
||||||
|
function showUserSelector() {
|
||||||
|
if (!userScopeDeptID.value) {
|
||||||
|
alert('请先选择作用域部门');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectorType.value = 'user';
|
||||||
|
selectorVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 选择器确认回调 */
|
||||||
|
function handleSelectorConfirm(items: Record<string, any>[]) {
|
||||||
|
const typedItems = items as TargetOption[];
|
||||||
|
|
||||||
|
if (selectorType.value === 'dept') {
|
||||||
|
selectedDepts.value = typedItems;
|
||||||
|
} else if (selectorType.value === 'role') {
|
||||||
|
selectedRoles.value = typedItems;
|
||||||
|
} else {
|
||||||
|
selectedUsers.value = typedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
emitTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移除部门 */
|
||||||
|
function removeDept(id: string) {
|
||||||
|
selectedDepts.value = selectedDepts.value.filter(item => item.id !== id);
|
||||||
|
emitTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移除角色 */
|
||||||
|
function removeRole(id: string) {
|
||||||
|
selectedRoles.value = selectedRoles.value.filter(item => item.id !== id);
|
||||||
|
emitTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移除用户 */
|
||||||
|
function removeUser(id: string) {
|
||||||
|
selectedUsers.value = selectedUsers.value.filter(item => item.id !== id);
|
||||||
|
emitTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 角色作用域变更 */
|
||||||
|
function onRoleScopeChange() {
|
||||||
|
// 清空已选择的角色
|
||||||
|
selectedRoles.value = [];
|
||||||
|
emitTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户作用域变更 */
|
||||||
|
function onUserScopeChange() {
|
||||||
|
// 清空已选择的用户
|
||||||
|
selectedUsers.value = [];
|
||||||
|
emitTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发射目标配置 */
|
||||||
|
function emitTargets() {
|
||||||
|
const targets: TbSysMessageTarget[] = [];
|
||||||
|
|
||||||
|
// 添加部门目标
|
||||||
|
selectedDepts.value.forEach(dept => {
|
||||||
|
targets.push({
|
||||||
|
sendMethod: props.sendMethod,
|
||||||
|
targetType: 'dept',
|
||||||
|
targetID: dept.id,
|
||||||
|
targetName: dept.name,
|
||||||
|
scopeDeptID: dept.id // 部门的作用域就是自己
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加角色目标
|
||||||
|
selectedRoles.value.forEach(role => {
|
||||||
|
targets.push({
|
||||||
|
sendMethod: props.sendMethod,
|
||||||
|
targetType: 'role',
|
||||||
|
targetID: role.id,
|
||||||
|
targetName: role.name,
|
||||||
|
scopeDeptID: roleScopeDeptID.value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加用户目标
|
||||||
|
selectedUsers.value.forEach(user => {
|
||||||
|
targets.push({
|
||||||
|
sendMethod: props.sendMethod,
|
||||||
|
targetType: 'user',
|
||||||
|
targetID: user.id,
|
||||||
|
targetName: user.name,
|
||||||
|
scopeDeptID: userScopeDeptID.value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('update:modelValue', targets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化已选项(从modelValue恢复) */
|
||||||
|
function initSelectedTargets() {
|
||||||
|
const targets = props.modelValue || [];
|
||||||
|
|
||||||
|
selectedDepts.value = [];
|
||||||
|
selectedRoles.value = [];
|
||||||
|
selectedUsers.value = [];
|
||||||
|
|
||||||
|
targets.forEach(target => {
|
||||||
|
const option: TargetOption = {
|
||||||
|
id: target.targetID!,
|
||||||
|
name: '', // 名称需要从后端查询,暂时为空
|
||||||
|
deptID: target.scopeDeptID,
|
||||||
|
deptName: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
if (target.targetType === 'dept') {
|
||||||
|
selectedDepts.value.push(option);
|
||||||
|
} else if (target.targetType === 'role') {
|
||||||
|
if (target.scopeDeptID) {
|
||||||
|
roleScopeDeptID.value = target.scopeDeptID;
|
||||||
|
}
|
||||||
|
selectedRoles.value.push(option);
|
||||||
|
} else if (target.targetType === 'user') {
|
||||||
|
if (target.scopeDeptID) {
|
||||||
|
userScopeDeptID.value = target.scopeDeptID;
|
||||||
|
}
|
||||||
|
selectedUsers.value.push(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听sendMethod变化,更新所有目标的sendMethod */
|
||||||
|
watch(() => props.sendMethod, () => {
|
||||||
|
emitTargets();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 监听modelValue变化,同步到内部状态 */
|
||||||
|
watch(() => props.modelValue, () => {
|
||||||
|
initSelectedTargets();
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadAvailableDepts();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-target-selector {
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.target-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #c8232c;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 2px solid #c8232c;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-content {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 200px;
|
||||||
|
|
||||||
|
.target-panel {
|
||||||
|
.scope-selector {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dept-select {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 200px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #c8232c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-select {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: #c8232c;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #a01d24;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 100px;
|
||||||
|
|
||||||
|
.selected-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-sublabel {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
margin-left: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #999;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #c00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
10
schoolNewsWeb/src/components/message/index.ts
Normal file
10
schoolNewsWeb/src/components/message/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* @description 消息通知组件导出
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as MessageTargetSelector } from './MessageTargetSelector.vue';
|
||||||
|
export { default as MessageSendMethodSelector } from './MessageSendMethodSelector.vue';
|
||||||
|
export { default as MessagePriorityBadge } from './MessagePriorityBadge.vue';
|
||||||
|
export { default as MessageStatusBadge } from './MessageStatusBadge.vue';
|
||||||
@@ -48,6 +48,9 @@ export * from './usercenter';
|
|||||||
// 定时任务相关
|
// 定时任务相关
|
||||||
export * from './crontab';
|
export * from './crontab';
|
||||||
|
|
||||||
|
// 消息通知相关
|
||||||
|
export * from './message';
|
||||||
|
|
||||||
// 日志相关
|
// 日志相关
|
||||||
export * from './log';
|
export * from './log';
|
||||||
|
|
||||||
|
|||||||
141
schoolNewsWeb/src/types/message/index.ts
Normal file
141
schoolNewsWeb/src/types/message/index.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* @description 消息通知类型定义
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BaseDTO } from '../base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息主体实体(对应后端 TbSysMessage)
|
||||||
|
*/
|
||||||
|
export interface TbSysMessage extends BaseDTO {
|
||||||
|
/** 消息ID(业务主键) */
|
||||||
|
messageID?: string;
|
||||||
|
/** 消息标题 */
|
||||||
|
title?: string;
|
||||||
|
/** 消息内容 */
|
||||||
|
content?: string;
|
||||||
|
/** 消息类型:notice-通知/announcement-公告/warning-预警/system-系统消息 */
|
||||||
|
messageType?: string;
|
||||||
|
/** 优先级:urgent-紧急/important-重要/normal-普通 */
|
||||||
|
priority?: string;
|
||||||
|
/** 发送人ID */
|
||||||
|
senderID?: string;
|
||||||
|
/** 发送人姓名 */
|
||||||
|
senderName?: string;
|
||||||
|
/** 发送人部门ID */
|
||||||
|
senderDeptID?: string;
|
||||||
|
/** 发送人部门名称 */
|
||||||
|
senderDeptName?: string;
|
||||||
|
/** 发送模式:immediate-立即发送/scheduled-定时发送 */
|
||||||
|
sendMode?: string;
|
||||||
|
/** 定时发送时间 */
|
||||||
|
scheduledTime?: string;
|
||||||
|
/** 实际发送时间 */
|
||||||
|
actualSendTime?: string;
|
||||||
|
/** 消息状态:draft-草稿/pending-待发送/sending-发送中/sent-已发送/failed-发送失败/cancelled-已取消 */
|
||||||
|
status?: string;
|
||||||
|
/** 目标用户数量 */
|
||||||
|
targetUserCount?: number;
|
||||||
|
/** 已发送数量 */
|
||||||
|
sentCount?: number;
|
||||||
|
/** 发送成功数量 */
|
||||||
|
successCount?: number;
|
||||||
|
/** 发送失败数量 */
|
||||||
|
failedCount?: number;
|
||||||
|
/** 已读数量 */
|
||||||
|
readCount?: number;
|
||||||
|
/** 重试次数 */
|
||||||
|
retryCount?: number;
|
||||||
|
/** 最大重试次数 */
|
||||||
|
maxRetryCount?: number;
|
||||||
|
/** 最后错误信息 */
|
||||||
|
lastError?: string;
|
||||||
|
/** 创建人 */
|
||||||
|
creator?: string;
|
||||||
|
/** 更新人 */
|
||||||
|
updater?: string;
|
||||||
|
/** 发送方式列表(前端辅助字段,从targets聚合) */
|
||||||
|
sendMethods?: string[];
|
||||||
|
/** 发送目标配置列表(前端辅助字段) */
|
||||||
|
targets?: TbSysMessageTarget[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息接收对象实体(对应后端 TbSysMessageTarget)
|
||||||
|
*/
|
||||||
|
export interface TbSysMessageTarget extends BaseDTO {
|
||||||
|
/** 消息ID */
|
||||||
|
messageID?: string;
|
||||||
|
/** 发送方式:system-系统消息/email-邮件/sms-短信 */
|
||||||
|
sendMethod?: string;
|
||||||
|
/** 接收对象类型:dept-部门/role-角色/user-用户 */
|
||||||
|
targetType?: string;
|
||||||
|
/** 接收对象ID */
|
||||||
|
targetID?: string;
|
||||||
|
/** 接收对象名称 */
|
||||||
|
targetName?: string;
|
||||||
|
/** 作用域部门ID(限制发送范围,防止越权) */
|
||||||
|
scopeDeptID?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户接收消息实体(对应后端 TbSysMessageUser)
|
||||||
|
*/
|
||||||
|
export interface TbSysMessageUser extends BaseDTO {
|
||||||
|
/** 消息ID */
|
||||||
|
messageID?: string;
|
||||||
|
/** 用户ID */
|
||||||
|
userID?: string;
|
||||||
|
/** 发送方式:system-系统消息/email-邮件/sms-短信 */
|
||||||
|
sendMethod?: string;
|
||||||
|
/** 是否已读 */
|
||||||
|
isRead?: boolean;
|
||||||
|
/** 已读时间 */
|
||||||
|
readTime?: string;
|
||||||
|
/** 发送状态:pending-待发送/success-发送成功/failed-发送失败 */
|
||||||
|
sendStatus?: string;
|
||||||
|
/** 失败原因 */
|
||||||
|
failReason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息VO(对应后端 MessageVO)
|
||||||
|
*/
|
||||||
|
export interface MessageVO extends TbSysMessage {
|
||||||
|
/** 发送进度百分比 */
|
||||||
|
sendProgress?: number;
|
||||||
|
/** 发送成功率 */
|
||||||
|
successRate?: number;
|
||||||
|
/** 已读率 */
|
||||||
|
readRate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户消息VO(对应后端 MessageUserVO)
|
||||||
|
*/
|
||||||
|
export interface MessageUserVO extends TbSysMessageUser {
|
||||||
|
/** 用户名 */
|
||||||
|
username?: string;
|
||||||
|
/** 用户真实姓名 */
|
||||||
|
realName?: string;
|
||||||
|
/** 用户邮箱 */
|
||||||
|
email?: string;
|
||||||
|
/** 用户手机 */
|
||||||
|
phone?: string;
|
||||||
|
/** 消息标题 */
|
||||||
|
title?: string;
|
||||||
|
/** 消息内容 */
|
||||||
|
content?: string;
|
||||||
|
/** 消息类型 */
|
||||||
|
messageType?: string;
|
||||||
|
/** 优先级 */
|
||||||
|
priority?: string;
|
||||||
|
/** 发送人姓名 */
|
||||||
|
senderName?: string;
|
||||||
|
/** 发送人部门名称 */
|
||||||
|
senderDeptName?: string;
|
||||||
|
/** 实际发送时间 */
|
||||||
|
actualSendTime?: string;
|
||||||
|
}
|
||||||
@@ -172,6 +172,18 @@ function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 当前菜单指定了页面组件时,即使存在子菜单也应当渲染该页面
|
||||||
|
if (menu.component) {
|
||||||
|
route.children!.push({
|
||||||
|
path: '',
|
||||||
|
name: `${menu.menuID}_page`,
|
||||||
|
component: getComponent(menu.component),
|
||||||
|
meta: {
|
||||||
|
...route.meta,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 只将普通子菜单加入 children
|
// 只将普通子菜单加入 children
|
||||||
normalChildren.forEach(child => {
|
normalChildren.forEach(child => {
|
||||||
const childRoute = generateRouteFromMenu(child, false);
|
const childRoute = generateRouteFromMenu(child, false);
|
||||||
|
|||||||
@@ -0,0 +1,737 @@
|
|||||||
|
<template>
|
||||||
|
<div class="message-manage-view">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<el-card class="search-card" shadow="never">
|
||||||
|
<el-form :model="searchForm" inline>
|
||||||
|
<el-form-item label="标题">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.title"
|
||||||
|
placeholder="请输入标题"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="消息类型">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.messageType"
|
||||||
|
placeholder="全部"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="通知" value="notice" />
|
||||||
|
<el-option label="公告" value="announcement" />
|
||||||
|
<el-option label="警告" value="warning" />
|
||||||
|
<el-option label="系统消息" value="system" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.status"
|
||||||
|
placeholder="全部"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="草稿" value="draft" />
|
||||||
|
<el-option label="待发送" value="pending" />
|
||||||
|
<el-option label="发送中" value="sending" />
|
||||||
|
<el-option label="已完成" value="completed" />
|
||||||
|
<el-option label="已取消" value="cancelled" />
|
||||||
|
<el-option label="失败" value="failed" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="优先级">
|
||||||
|
<el-select
|
||||||
|
v-model="searchForm.priority"
|
||||||
|
placeholder="全部"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option label="紧急" value="urgent" />
|
||||||
|
<el-option label="重要" value="important" />
|
||||||
|
<el-option label="普通" value="normal" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">
|
||||||
|
<el-icon><RefreshLeft /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<el-card class="operation-card" shadow="never">
|
||||||
|
<el-button type="primary" @click="handleCreate">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
创建消息
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="loadMessages">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 消息列表 -->
|
||||||
|
<el-card class="table-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="messageList"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column label="类型" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getMessageTypeTag(row.messageType || '')">
|
||||||
|
{{ getMessageTypeLabel(row.messageType || '') }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="优先级" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<MessagePriorityBadge :priority="row.priority" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<MessageStatusBadge :status="row.status || ''" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="发送方式" width="150" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag
|
||||||
|
v-for="method in row.sendMethods"
|
||||||
|
:key="method"
|
||||||
|
size="small"
|
||||||
|
style="margin: 2px"
|
||||||
|
>
|
||||||
|
{{ getSendMethodLabel(method) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="发送时间" width="180" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.scheduledTime || '立即发送' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="创建时间" width="180" align="center" prop="createdAt" />
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="300" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="handleViewDetail(row)"
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 'draft'"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="handleEdit(row)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="['pending', 'draft'].includes(row.status || '')"
|
||||||
|
link
|
||||||
|
type="success"
|
||||||
|
@click="handleSend(row)"
|
||||||
|
>
|
||||||
|
立即发送
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 'failed'"
|
||||||
|
link
|
||||||
|
type="warning"
|
||||||
|
@click="handleRetry(row)"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 'pending'"
|
||||||
|
link
|
||||||
|
type="warning"
|
||||||
|
@click="handleReschedule(row)"
|
||||||
|
>
|
||||||
|
改期
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="['pending', 'sending'].includes(row.status || '')"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleCancel(row)"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="['draft', 'cancelled', 'failed'].includes(row.status || '')"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="loadMessages"
|
||||||
|
@current-change="loadMessages"
|
||||||
|
style="margin-top: 20px; justify-content: flex-end"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 创建/编辑对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="createDialogVisible"
|
||||||
|
:title="isEdit ? '编辑消息' : '创建消息'"
|
||||||
|
width="900px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<MessageAdd
|
||||||
|
ref="messageAddRef"
|
||||||
|
:model-value="currentMessage"
|
||||||
|
:is-edit="isEdit"
|
||||||
|
/>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||||
|
<el-button @click="handleSaveDraft">保存草稿</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">
|
||||||
|
{{ isEdit ? '更新' : '创建并发送' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
title="消息详情"
|
||||||
|
width="900px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div v-if="currentDetail" class="detail-content">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<el-descriptions title="基本信息" :column="2" border>
|
||||||
|
<el-descriptions-item label="标题">
|
||||||
|
{{ currentDetail.title }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="消息类型">
|
||||||
|
<el-tag :type="getMessageTypeTag(currentDetail.messageType || '')">
|
||||||
|
{{ getMessageTypeLabel(currentDetail.messageType || '') }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="优先级">
|
||||||
|
<MessagePriorityBadge :priority="currentDetail.priority || ''" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<MessageStatusBadge :status="currentDetail.status || ''" />
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="发送方式" :span="2">
|
||||||
|
<el-tag
|
||||||
|
v-for="method in currentDetail.sendMethods"
|
||||||
|
:key="method"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
>
|
||||||
|
{{ getSendMethodLabel(method) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="内容" :span="2">
|
||||||
|
<div style="white-space: pre-wrap">{{ currentDetail.content }}</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="计划发送时间">
|
||||||
|
{{ currentDetail.scheduledTime || '立即发送' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="实际发送时间">
|
||||||
|
{{ currentDetail.actualSendTime || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">
|
||||||
|
{{ currentDetail.createTime }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">
|
||||||
|
{{ currentDetail.updateTime }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<!-- 发送统计 -->
|
||||||
|
<el-divider content-position="left">发送统计</el-divider>
|
||||||
|
<MessageStatistic :statistics="currentDetail" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 改期对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="rescheduleDialogVisible"
|
||||||
|
title="改期发送"
|
||||||
|
width="500px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="新发送时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="newScheduledTime"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="选择新的发送时间"
|
||||||
|
:disabled-date="disablePastDate"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="rescheduleDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmReschedule">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
RefreshLeft,
|
||||||
|
Plus,
|
||||||
|
Refresh
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import { messageApi } from '@/apis/message';
|
||||||
|
import { MessagePriorityBadge, MessageStatusBadge } from '@/components/message';
|
||||||
|
import { MessageAdd, MessageStatistic } from './components';
|
||||||
|
import type { MessageVO, TbSysMessage } from '@/types';
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive<Partial<TbSysMessage> & { page?: number; size?: number }>({
|
||||||
|
title: '',
|
||||||
|
messageType: undefined,
|
||||||
|
status: undefined,
|
||||||
|
priority: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 列表数据
|
||||||
|
const loading = ref(false);
|
||||||
|
const messageList = ref<MessageVO[]>([]);
|
||||||
|
|
||||||
|
// 创建/编辑对话框
|
||||||
|
const createDialogVisible = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
const messageAddRef = ref();
|
||||||
|
const currentMessage = ref<TbSysMessage>({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
messageType: 'notice',
|
||||||
|
priority: 'normal',
|
||||||
|
sendMode: 'immediate',
|
||||||
|
scheduledTime: '',
|
||||||
|
maxRetryCount: 3,
|
||||||
|
sendMethods: ['system'],
|
||||||
|
targets: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// 详情对话框
|
||||||
|
const detailDialogVisible = ref(false);
|
||||||
|
const currentDetail = ref<MessageVO | null>(null);
|
||||||
|
|
||||||
|
// 改期对话框
|
||||||
|
const rescheduleDialogVisible = ref(false);
|
||||||
|
const currentRescheduleId = ref('');
|
||||||
|
const newScheduledTime = ref('');
|
||||||
|
|
||||||
|
/** 加载消息列表 */
|
||||||
|
async function loadMessages() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await messageApi.getMessagePage({
|
||||||
|
pageParam: {
|
||||||
|
pageNumber: pagination.page,
|
||||||
|
pageSize: pagination.size
|
||||||
|
},
|
||||||
|
filter: searchForm
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
messageList.value = result.dataList || [];
|
||||||
|
pagination.total = result.pageDomain?.pageParam.totalElements || 0;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '加载失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载消息列表失败:', error);
|
||||||
|
ElMessage.error('加载失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索 */
|
||||||
|
function handleSearch() {
|
||||||
|
pagination.page = 1;
|
||||||
|
loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置搜索 */
|
||||||
|
function handleReset() {
|
||||||
|
Object.assign(searchForm, {
|
||||||
|
title: '',
|
||||||
|
messageType: undefined,
|
||||||
|
status: undefined,
|
||||||
|
priority: undefined
|
||||||
|
});
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建消息 */
|
||||||
|
function handleCreate() {
|
||||||
|
isEdit.value = false;
|
||||||
|
currentMessage.value = {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
messageType: 'notice',
|
||||||
|
priority: 'normal',
|
||||||
|
sendMode: 'immediate',
|
||||||
|
scheduledTime: '',
|
||||||
|
maxRetryCount: 3,
|
||||||
|
sendMethods: ['system'],
|
||||||
|
targets: []
|
||||||
|
};
|
||||||
|
createDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑消息 */
|
||||||
|
async function handleEdit(row: MessageVO) {
|
||||||
|
try {
|
||||||
|
const result = await messageApi.getMessageDetail(row.messageID!);
|
||||||
|
if (result.success && result.data) {
|
||||||
|
isEdit.value = true;
|
||||||
|
currentMessage.value = {
|
||||||
|
messageID: result.data.messageID,
|
||||||
|
title: result.data.title ?? '',
|
||||||
|
content: result.data.content ?? '',
|
||||||
|
messageType: result.data.messageType ?? 'notice',
|
||||||
|
priority: result.data.priority ?? 'normal',
|
||||||
|
sendMode: result.data.sendMode ?? 'immediate',
|
||||||
|
scheduledTime: result.data.scheduledTime,
|
||||||
|
maxRetryCount: result.data.maxRetryCount,
|
||||||
|
sendMethods: result.data.sendMethods ?? ['system'],
|
||||||
|
targets: result.data.targets || []
|
||||||
|
};
|
||||||
|
createDialogVisible.value = true;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '加载失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载消息详情失败:', error);
|
||||||
|
ElMessage.error('加载失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存草稿 */
|
||||||
|
async function handleSaveDraft() {
|
||||||
|
const valid = await messageAddRef.value?.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
const formData = messageAddRef.value?.getFormData();
|
||||||
|
if (!formData) return;
|
||||||
|
|
||||||
|
const draftData = { ...formData, status: 'draft' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 如果 currentMessage 有 messageID,说明是编辑模式,需要带上 messageID
|
||||||
|
if (isEdit.value && currentMessage.value.messageID) {
|
||||||
|
draftData.messageID = currentMessage.value.messageID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据是否有 messageID 判断是创建还是更新
|
||||||
|
const result = draftData.messageID
|
||||||
|
? await messageApi.updateMessage(draftData)
|
||||||
|
: await messageApi.createMessage(draftData);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
createDialogVisible.value = false;
|
||||||
|
loadMessages();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '保存失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存草稿失败:', error);
|
||||||
|
ElMessage.error('保存失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交(创建并发送或更新) */
|
||||||
|
async function handleSubmit() {
|
||||||
|
const valid = await messageAddRef.value?.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
const formData = messageAddRef.value?.getFormData();
|
||||||
|
if (!formData) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 如果 currentMessage 有 messageID,说明是编辑模式,需要带上 messageID
|
||||||
|
if (isEdit.value && currentMessage.value.messageID) {
|
||||||
|
formData.messageID = currentMessage.value.messageID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据是否有 messageID 判断是创建还是更新
|
||||||
|
const result = formData.messageID
|
||||||
|
? await messageApi.updateMessage(formData)
|
||||||
|
: await messageApi.createMessage(formData);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success(formData.messageID ? '更新成功' : '创建成功');
|
||||||
|
createDialogVisible.value = false;
|
||||||
|
loadMessages();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || (formData.messageID ? '更新失败' : '创建失败'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交失败:', error);
|
||||||
|
ElMessage.error('操作失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看详情 */
|
||||||
|
async function handleViewDetail(row: MessageVO) {
|
||||||
|
try {
|
||||||
|
const result = await messageApi.getMessage(row.messageID!);
|
||||||
|
if (result.success && result.data) {
|
||||||
|
currentDetail.value = result.data;
|
||||||
|
detailDialogVisible.value = true;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '加载失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载消息详情失败:', error);
|
||||||
|
ElMessage.error('加载失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 立即发送 */
|
||||||
|
async function handleSend(row: MessageVO) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要立即发送该消息吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await messageApi.sendMessage(row.messageID!);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('发送成功');
|
||||||
|
loadMessages();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '发送失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('发送失败:', error);
|
||||||
|
ElMessage.error('发送失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重试 */
|
||||||
|
async function handleRetry(row: MessageVO) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要重试发送该消息吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await messageApi.retryMessage(row.messageID!);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('已提交重试');
|
||||||
|
loadMessages();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '重试失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('重试失败:', error);
|
||||||
|
ElMessage.error('重试失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 改期 */
|
||||||
|
function handleReschedule(row: MessageVO) {
|
||||||
|
currentRescheduleId.value = row.messageID!;
|
||||||
|
newScheduledTime.value = row.scheduledTime || '';
|
||||||
|
rescheduleDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 确认改期 */
|
||||||
|
async function confirmReschedule() {
|
||||||
|
if (!newScheduledTime.value) {
|
||||||
|
ElMessage.warning('请选择新的发送时间');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await messageApi.rescheduleMessage(
|
||||||
|
currentRescheduleId.value,
|
||||||
|
newScheduledTime.value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('改期成功');
|
||||||
|
rescheduleDialogVisible.value = false;
|
||||||
|
loadMessages();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '改期失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('改期失败:', error);
|
||||||
|
ElMessage.error('改期失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消 */
|
||||||
|
async function handleCancel(row: MessageVO) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要取消该消息发送吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await messageApi.cancelMessage(row.messageID!);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('已取消');
|
||||||
|
loadMessages();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '取消失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('取消失败:', error);
|
||||||
|
ElMessage.error('取消失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除 */
|
||||||
|
async function handleDelete(row: MessageVO) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除该消息吗?删除后无法恢复!', '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await messageApi.deleteMessage(row.messageID!);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
loadMessages();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
ElMessage.error('删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 禁用过去日期 */
|
||||||
|
function disablePastDate(time: Date): boolean {
|
||||||
|
return time.getTime() < Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取消息类型标签 */
|
||||||
|
function getMessageTypeTag(type: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
notice: '',
|
||||||
|
announcement: 'success',
|
||||||
|
warning: 'warning',
|
||||||
|
system: 'info'
|
||||||
|
};
|
||||||
|
return map[type] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取消息类型文本 */
|
||||||
|
function getMessageTypeLabel(type: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
notice: '通知',
|
||||||
|
announcement: '公告',
|
||||||
|
warning: '警告',
|
||||||
|
system: '系统消息'
|
||||||
|
};
|
||||||
|
return map[type] || type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取发送方式文本 */
|
||||||
|
function getSendMethodLabel(method: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
system: '系统消息',
|
||||||
|
email: '邮件通知',
|
||||||
|
sms: '短信通知'
|
||||||
|
};
|
||||||
|
return map[method] || method;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadMessages();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-manage-view {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.search-card,
|
||||||
|
.operation-card,
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,572 @@
|
|||||||
|
<template>
|
||||||
|
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<el-divider content-position="left">基本信息</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="消息标题" prop="title">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.title"
|
||||||
|
placeholder="请输入消息标题"
|
||||||
|
maxlength="100"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="消息类型" prop="messageType">
|
||||||
|
<el-radio-group v-model="formData.messageType">
|
||||||
|
<el-radio value="notice">通知</el-radio>
|
||||||
|
<el-radio value="announcement">公告</el-radio>
|
||||||
|
<el-radio value="warning">警告</el-radio>
|
||||||
|
<el-radio value="system">系统消息</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="优先级" prop="priority">
|
||||||
|
<el-radio-group v-model="formData.priority">
|
||||||
|
<el-radio value="urgent">紧急</el-radio>
|
||||||
|
<el-radio value="important">重要</el-radio>
|
||||||
|
<el-radio value="normal">普通</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="消息内容" prop="content">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
placeholder="请输入消息内容"
|
||||||
|
maxlength="2000"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 发送设置 -->
|
||||||
|
<el-divider content-position="left">发送设置</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="发送模式" prop="sendMode">
|
||||||
|
<el-radio-group v-model="formData.sendMode">
|
||||||
|
<el-radio value="immediate">立即发送</el-radio>
|
||||||
|
<el-radio value="scheduled">定时发送</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item
|
||||||
|
v-if="formData.sendMode === 'scheduled'"
|
||||||
|
label="发送时间"
|
||||||
|
prop="scheduledTime"
|
||||||
|
>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="formData.scheduledTime"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="选择发送时间"
|
||||||
|
:disabled-date="disablePastDate"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="最大重试次数" prop="maxRetryCount">
|
||||||
|
<el-input-number
|
||||||
|
v-model="formData.maxRetryCount"
|
||||||
|
:min="0"
|
||||||
|
:max="5"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
<span class="form-tip">发送失败后重试次数(0-5次)</span>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 发送方式 -->
|
||||||
|
<el-divider content-position="left">发送方式</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="发送渠道" prop="sendMethods">
|
||||||
|
<el-checkbox-group v-model="formData.sendMethods">
|
||||||
|
<el-checkbox value="system">系统消息</el-checkbox>
|
||||||
|
<el-checkbox value="email">邮件通知</el-checkbox>
|
||||||
|
<el-checkbox value="sms">短信通知</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 接收对象 -->
|
||||||
|
<el-divider content-position="left">接收对象</el-divider>
|
||||||
|
|
||||||
|
<el-form-item label="选择对象">
|
||||||
|
<el-button type="primary" size="small" @click="deptSelectorVisible = true">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
选择部门
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" size="small" @click="deptRoleSelectorVisible = true" style="margin-left: 8px">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
选择部门角色
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" size="small" @click="userSelectorVisible = true" style="margin-left: 8px">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
选择用户
|
||||||
|
</el-button>
|
||||||
|
<span class="form-tip">已选择 {{ getTotalTargetCount() }} 个对象</span>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div v-if="formData.targets && formData.targets.length > 0" class="selected-targets">
|
||||||
|
<el-tag
|
||||||
|
v-for="(target, index) in formData.targets"
|
||||||
|
:key="index"
|
||||||
|
closable
|
||||||
|
@close="removeTarget(index)"
|
||||||
|
style="margin: 4px"
|
||||||
|
>
|
||||||
|
{{ getTargetDisplayName(target) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 部门选择器 -->
|
||||||
|
<GenericSelector
|
||||||
|
v-model:visible="deptSelectorVisible"
|
||||||
|
title="选择部门"
|
||||||
|
:fetch-available-api="fetchAllDepts"
|
||||||
|
:fetch-selected-api="fetchSelectedDepts"
|
||||||
|
:filter-selected="filterDepts"
|
||||||
|
:item-config="{ id: 'id', label: 'name' }"
|
||||||
|
unit-name="个部门"
|
||||||
|
@confirm="handleDeptConfirm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 部门角色选择器 -->
|
||||||
|
<GenericSelector
|
||||||
|
v-model:visible="deptRoleSelectorVisible"
|
||||||
|
title="选择部门角色"
|
||||||
|
:fetch-available-api="fetchAllDeptRoles"
|
||||||
|
:fetch-selected-api="fetchSelectedDeptRoles"
|
||||||
|
:filter-selected="filterDeptRoles"
|
||||||
|
:item-config="{ id: 'combinedId', label: 'displayName' }"
|
||||||
|
:use-tree="true"
|
||||||
|
:tree-transform="transformDeptRolesToTree"
|
||||||
|
:tree-props="{ children: 'children', label: 'displayName', id: 'combinedId' }"
|
||||||
|
:only-leaf-selectable="true"
|
||||||
|
unit-name="个部门角色"
|
||||||
|
@confirm="handleDeptRoleConfirm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 用户选择器 -->
|
||||||
|
<GenericSelector
|
||||||
|
v-model:visible="userSelectorVisible"
|
||||||
|
title="选择用户"
|
||||||
|
:fetch-available-api="fetchAllUsers"
|
||||||
|
:fetch-selected-api="fetchSelectedUsers"
|
||||||
|
:filter-selected="filterUsers"
|
||||||
|
:item-config="{ id: 'id', label: 'displayName', sublabel: 'deptName' }"
|
||||||
|
unit-name="个用户"
|
||||||
|
@confirm="handleUserConfirm"
|
||||||
|
/>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||||
|
import { Plus } from '@element-plus/icons-vue';
|
||||||
|
import type { TbSysMessage, TbSysMessageTarget } from '@/types';
|
||||||
|
import { GenericSelector } from '@/components/base';
|
||||||
|
import { deptApi, roleApi, userApi } from '@/apis/system';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: TbSysMessage;
|
||||||
|
isEdit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
isEdit: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>();
|
||||||
|
const formData = ref<TbSysMessage>({
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
messageType: 'notice',
|
||||||
|
priority: 'normal',
|
||||||
|
sendMode: 'immediate',
|
||||||
|
scheduledTime: '',
|
||||||
|
maxRetryCount: 3,
|
||||||
|
sendMethods: ['system'],
|
||||||
|
targets: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules: FormRules = {
|
||||||
|
title: [{ required: true, message: '请输入消息标题', trigger: 'blur' }],
|
||||||
|
content: [{ required: true, message: '请输入消息内容', trigger: 'blur' }],
|
||||||
|
messageType: [{ required: true, message: '请选择消息类型', trigger: 'change' }],
|
||||||
|
priority: [{ required: true, message: '请选择优先级', trigger: 'change' }],
|
||||||
|
sendMode: [{ required: true, message: '请选择发送模式', trigger: 'change' }],
|
||||||
|
scheduledTime: [{ required: true, message: '请选择发送时间', trigger: 'change' }],
|
||||||
|
sendMethods: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请至少选择一种发送方式',
|
||||||
|
trigger: 'change',
|
||||||
|
validator: (rule: any, value: any) => {
|
||||||
|
return Array.isArray(value) && value.length > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 目标选择相关
|
||||||
|
interface TargetOption {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
deptName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择器可见性状态
|
||||||
|
const deptSelectorVisible = ref(false);
|
||||||
|
const deptRoleSelectorVisible = ref(false);
|
||||||
|
const userSelectorVisible = ref(false);
|
||||||
|
|
||||||
|
/** 禁用过去日期 */
|
||||||
|
function disablePastDate(time: Date): boolean {
|
||||||
|
return time.getTime() < Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取总目标数量 */
|
||||||
|
function getTotalTargetCount(): number {
|
||||||
|
return formData.value.targets?.length || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取目标显示名称 */
|
||||||
|
function getTargetDisplayName(target: TbSysMessageTarget): string {
|
||||||
|
if (target.targetType === 'dept') {
|
||||||
|
return `部门: ${target.targetName || target.targetID}`;
|
||||||
|
} else if (target.targetType === 'role') {
|
||||||
|
return `部门角色: ${target.targetName || target.targetID}`;
|
||||||
|
} else if (target.targetType === 'user') {
|
||||||
|
return `用户: ${target.targetName || target.targetID}`;
|
||||||
|
}
|
||||||
|
return target.targetName || target.targetID || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移除目标 */
|
||||||
|
function removeTarget(index: number) {
|
||||||
|
if (formData.value.targets) {
|
||||||
|
formData.value.targets.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 部门选择相关 ====================
|
||||||
|
|
||||||
|
/** 获取所有可选部门 */
|
||||||
|
async function fetchAllDepts() {
|
||||||
|
const result = await deptApi.getAllDepts();
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
dataList: result.dataList.map((dept: any) => ({
|
||||||
|
id: dept.deptID,
|
||||||
|
name: dept.name
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取已选择的部门 */
|
||||||
|
async function fetchSelectedDepts() {
|
||||||
|
if (!formData.value.targets) {
|
||||||
|
return { success: true, dataList: [], code: 200, message: '', login: true, auth: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDepts = formData.value.targets
|
||||||
|
.filter(t => t.targetType === 'dept')
|
||||||
|
.map(t => ({
|
||||||
|
id: t.targetID,
|
||||||
|
name: t.targetName || t.targetID
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
dataList: selectedDepts,
|
||||||
|
code: 200,
|
||||||
|
message: '',
|
||||||
|
login: true,
|
||||||
|
auth: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 过滤已选择的部门 */
|
||||||
|
function filterDepts(available: any[], selected: any[]) {
|
||||||
|
const selectedIds = new Set(selected.map(item => item.id));
|
||||||
|
return available.filter(item => !selectedIds.has(item.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 部门选择确认 */
|
||||||
|
function handleDeptConfirm(items: any[]) {
|
||||||
|
// 移除旧的部门类型目标
|
||||||
|
if (!formData.value.targets) {
|
||||||
|
formData.value.targets = [];
|
||||||
|
}
|
||||||
|
formData.value.targets = formData.value.targets.filter(t => t.targetType !== 'dept');
|
||||||
|
|
||||||
|
// 添加新选择的部门
|
||||||
|
items.forEach(dept => {
|
||||||
|
formData.value.targets!.push({
|
||||||
|
targetType: 'dept',
|
||||||
|
targetID: dept.id,
|
||||||
|
targetName: dept.name,
|
||||||
|
scopeDeptID: dept.id, // 部门的作用域就是自己
|
||||||
|
sendMethod: formData.value.sendMethods?.[0] || 'system'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 部门角色选择相关 ====================
|
||||||
|
|
||||||
|
/** 获取所有可选部门角色 */
|
||||||
|
async function fetchAllDeptRoles() {
|
||||||
|
const result = await deptApi.getDeptRoleList({} as any);
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
const transformed = result.dataList
|
||||||
|
.filter((item: any) => item.deptID && item.roleID)
|
||||||
|
.map((item: any) => ({
|
||||||
|
...item,
|
||||||
|
combinedId: `${item.deptID}-${item.roleID}`,
|
||||||
|
displayName: `${item.deptName || ''} - ${item.roleName || ''}`,
|
||||||
|
deptDescription: item.deptDescription || ''
|
||||||
|
}));
|
||||||
|
return { ...result, dataList: transformed };
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取已选择的部门角色 */
|
||||||
|
async function fetchSelectedDeptRoles() {
|
||||||
|
if (!formData.value.targets) {
|
||||||
|
return { success: true, dataList: [], code: 200, message: '', login: true, auth: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedRoles = formData.value.targets
|
||||||
|
.filter(t => t.targetType === 'role')
|
||||||
|
.map(t => ({
|
||||||
|
deptID: t.scopeDeptID,
|
||||||
|
roleID: t.targetID,
|
||||||
|
combinedId: `${t.scopeDeptID}-${t.targetID}`,
|
||||||
|
displayName: t.targetName || `${t.scopeDeptID}-${t.targetID}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
dataList: selectedRoles,
|
||||||
|
code: 200,
|
||||||
|
message: '',
|
||||||
|
login: true,
|
||||||
|
auth: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 过滤已选择的部门角色 */
|
||||||
|
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[]) {
|
||||||
|
const deptMap = new Map<string, any>();
|
||||||
|
const tree: any[] = [];
|
||||||
|
|
||||||
|
flatData.forEach(item => {
|
||||||
|
const deptID = item.deptID;
|
||||||
|
|
||||||
|
if (!deptMap.has(deptID)) {
|
||||||
|
deptMap.set(deptID, {
|
||||||
|
combinedId: deptID,
|
||||||
|
displayName: item.deptName || deptID,
|
||||||
|
deptDescription: item.deptDescription,
|
||||||
|
children: [],
|
||||||
|
isDept: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deptNode = deptMap.get(deptID);
|
||||||
|
if (deptNode) {
|
||||||
|
deptNode.children.push({
|
||||||
|
...item,
|
||||||
|
isDept: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deptMap.forEach(deptNode => {
|
||||||
|
tree.push(deptNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 部门角色选择确认 */
|
||||||
|
function handleDeptRoleConfirm(items: any[]) {
|
||||||
|
// 移除旧的角色类型目标
|
||||||
|
if (!formData.value.targets) {
|
||||||
|
formData.value.targets = [];
|
||||||
|
}
|
||||||
|
formData.value.targets = formData.value.targets.filter(t => t.targetType !== 'role');
|
||||||
|
|
||||||
|
// 添加新选择的部门角色
|
||||||
|
items.forEach(role => {
|
||||||
|
formData.value.targets!.push({
|
||||||
|
targetType: 'role',
|
||||||
|
targetID: role.roleID,
|
||||||
|
targetName: role.displayName,
|
||||||
|
scopeDeptID: role.deptID,
|
||||||
|
sendMethod: formData.value.sendMethods?.[0] || 'system'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 用户选择相关 ====================
|
||||||
|
|
||||||
|
/** 获取所有可选用户 */
|
||||||
|
async function fetchAllUsers() {
|
||||||
|
const result = await userApi.getUserList({} as any);
|
||||||
|
if (result.success && result.dataList) {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
dataList: result.dataList.map((user: any) => ({
|
||||||
|
id: user.id || user.userID,
|
||||||
|
displayName: user.realName || user.username,
|
||||||
|
deptID: user.deptID,
|
||||||
|
deptName: user.deptName
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取已选择的用户 */
|
||||||
|
async function fetchSelectedUsers() {
|
||||||
|
if (!formData.value.targets) {
|
||||||
|
return { success: true, dataList: [], code: 200, message: '', login: true, auth: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedUsers = formData.value.targets
|
||||||
|
.filter(t => t.targetType === 'user')
|
||||||
|
.map(t => ({
|
||||||
|
id: t.targetID,
|
||||||
|
displayName: t.targetName || t.targetID,
|
||||||
|
deptID: t.scopeDeptID,
|
||||||
|
deptName: ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
dataList: selectedUsers,
|
||||||
|
code: 200,
|
||||||
|
message: '',
|
||||||
|
login: true,
|
||||||
|
auth: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 过滤已选择的用户 */
|
||||||
|
function filterUsers(available: any[], selected: any[]) {
|
||||||
|
const selectedIds = new Set(selected.map(item => item.id));
|
||||||
|
return available.filter(item => !selectedIds.has(item.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户选择确认 */
|
||||||
|
function handleUserConfirm(items: any[]) {
|
||||||
|
// 移除旧的用户类型目标
|
||||||
|
if (!formData.value.targets) {
|
||||||
|
formData.value.targets = [];
|
||||||
|
}
|
||||||
|
formData.value.targets = formData.value.targets.filter(t => t.targetType !== 'user');
|
||||||
|
|
||||||
|
// 添加新选择的用户
|
||||||
|
items.forEach(user => {
|
||||||
|
formData.value.targets!.push({
|
||||||
|
targetType: 'user',
|
||||||
|
targetID: user.id,
|
||||||
|
targetName: user.displayName,
|
||||||
|
scopeDeptID: user.deptID,
|
||||||
|
sendMethod: formData.value.sendMethods?.[0] || 'system'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 表单操作 ====================
|
||||||
|
|
||||||
|
/** 表单验证 */
|
||||||
|
async function validate(): Promise<boolean> {
|
||||||
|
if (!formRef.value) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
|
||||||
|
// 验证目标
|
||||||
|
if (!formData.value.targets || formData.value.targets.length === 0) {
|
||||||
|
ElMessage.warning('请至少选择一个接收对象');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取表单数据 */
|
||||||
|
function getFormData(): TbSysMessage {
|
||||||
|
return formData.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
function reset() {
|
||||||
|
formData.value = {
|
||||||
|
title: '',
|
||||||
|
content: '',
|
||||||
|
messageType: 'notice',
|
||||||
|
priority: 'normal',
|
||||||
|
sendMode: 'immediate',
|
||||||
|
scheduledTime: '',
|
||||||
|
maxRetryCount: 3,
|
||||||
|
sendMethods: ['system'],
|
||||||
|
targets: []
|
||||||
|
};
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听modelValue变化,同步到formData
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if (val) {
|
||||||
|
formData.value = { ...val };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
validate,
|
||||||
|
getFormData,
|
||||||
|
reset
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.form-tip {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-targets {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-list {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
<template>
|
||||||
|
<div class="message-statistic">
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<el-row :gutter="16" class="statistic-cards">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon users">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-label">目标用户数</div>
|
||||||
|
<div class="card-value">{{ statistics.targetUserCount || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon sent">
|
||||||
|
<el-icon><Promotion /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-label">已发送</div>
|
||||||
|
<div class="card-value">{{ statistics.sentCount || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon success">
|
||||||
|
<el-icon><CircleCheck /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-label">发送成功</div>
|
||||||
|
<div class="card-value">{{ statistics.successCount || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row :gutter="16" class="statistic-cards">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon failed">
|
||||||
|
<el-icon><CircleClose /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-label">发送失败</div>
|
||||||
|
<div class="card-value">{{ statistics.failedCount || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon read">
|
||||||
|
<el-icon><View /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-label">已读人数</div>
|
||||||
|
<div class="card-value">{{ statistics.readCount || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-icon pending">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-label">待发送</div>
|
||||||
|
<div class="card-value">{{ pendingCount }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div class="progress-section">
|
||||||
|
<div class="progress-item">
|
||||||
|
<div class="progress-label">
|
||||||
|
<span>发送进度</span>
|
||||||
|
<span class="progress-value">{{ sendProgress }}%</span>
|
||||||
|
</div>
|
||||||
|
<el-progress
|
||||||
|
:percentage="sendProgress"
|
||||||
|
:color="progressColor"
|
||||||
|
:stroke-width="12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-item">
|
||||||
|
<div class="progress-label">
|
||||||
|
<span>成功率</span>
|
||||||
|
<span class="progress-value">{{ successRate }}%</span>
|
||||||
|
</div>
|
||||||
|
<el-progress
|
||||||
|
:percentage="successRate"
|
||||||
|
:color="successRateColor"
|
||||||
|
:stroke-width="12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-item">
|
||||||
|
<div class="progress-label">
|
||||||
|
<span>已读率</span>
|
||||||
|
<span class="progress-value">{{ readRate }}%</span>
|
||||||
|
</div>
|
||||||
|
<el-progress
|
||||||
|
:percentage="readRate"
|
||||||
|
color="#409eff"
|
||||||
|
:stroke-width="12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
Promotion,
|
||||||
|
CircleCheck,
|
||||||
|
CircleClose,
|
||||||
|
View,
|
||||||
|
Clock
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import type { MessageVO } from '@/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
statistics: MessageVO;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
/** 待发送数量 */
|
||||||
|
const pendingCount = computed(() => {
|
||||||
|
const target = props.statistics.targetUserCount || 0;
|
||||||
|
const sent = props.statistics.sentCount || 0;
|
||||||
|
return Math.max(0, target - sent);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 发送进度 */
|
||||||
|
const sendProgress = computed(() => {
|
||||||
|
const target = props.statistics.targetUserCount || 0;
|
||||||
|
const sent = props.statistics.sentCount || 0;
|
||||||
|
if (target === 0) return 0;
|
||||||
|
return Math.round((sent / target) * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 成功率 */
|
||||||
|
const successRate = computed(() => {
|
||||||
|
const sent = props.statistics.sentCount || 0;
|
||||||
|
const success = props.statistics.successCount || 0;
|
||||||
|
if (sent === 0) return 0;
|
||||||
|
return Math.round((success / sent) * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 已读率 */
|
||||||
|
const readRate = computed(() => {
|
||||||
|
const success = props.statistics.successCount || 0;
|
||||||
|
const read = props.statistics.readCount || 0;
|
||||||
|
if (success === 0) return 0;
|
||||||
|
return Math.round((read / success) * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 发送进度颜色 */
|
||||||
|
const progressColor = computed(() => {
|
||||||
|
const progress = sendProgress.value;
|
||||||
|
if (progress < 30) return '#f56c6c';
|
||||||
|
if (progress < 70) return '#e6a23c';
|
||||||
|
if (progress < 100) return '#409eff';
|
||||||
|
return '#67c23a';
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 成功率颜色 */
|
||||||
|
const successRateColor = computed(() => {
|
||||||
|
const rate = successRate.value;
|
||||||
|
if (rate < 60) return '#f56c6c';
|
||||||
|
if (rate < 90) return '#e6a23c';
|
||||||
|
return '#67c23a';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-statistic {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statistic-cards {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&.users {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sent {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.failed {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.read {
|
||||||
|
background: linear-gradient(135deg, #30cfd0 0%, #330867 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section {
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
|
||||||
|
.progress-value {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @description 消息管理组件导出
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as MessageAdd } from './MessageAdd.vue';
|
||||||
|
export { default as MessageStatistic } from './MessageStatistic.vue';
|
||||||
8
schoolNewsWeb/src/views/admin/manage/message/index.ts
Normal file
8
schoolNewsWeb/src/views/admin/manage/message/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @description 消息管理视图导出
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as MessageManageView } from './MessageManageView.vue';
|
||||||
|
export * from './components';
|
||||||
326
schoolNewsWeb/src/views/public/message/MessageDetail.vue
Normal file
326
schoolNewsWeb/src/views/public/message/MessageDetail.vue
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
<template>
|
||||||
|
<div v-loading="loading" class="message-detail">
|
||||||
|
<div v-if="messageData" class="detail-container">
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<el-button
|
||||||
|
v-if="showBackButton"
|
||||||
|
type="text"
|
||||||
|
@click="handleBack"
|
||||||
|
class="back-button"
|
||||||
|
>
|
||||||
|
<el-icon><ArrowLeft /></el-icon>
|
||||||
|
{{ backButtonText }}
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<!-- 消息头部 -->
|
||||||
|
<div class="message-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<h2 class="message-title">{{ messageData.title }}</h2>
|
||||||
|
<div class="message-meta">
|
||||||
|
<el-tag :type="getMessageTypeTag(messageData.messageType || '')" size="large">
|
||||||
|
{{ getMessageTypeLabel(messageData.messageType || '') }}
|
||||||
|
</el-tag>
|
||||||
|
<MessagePriorityBadge :priority="messageData.priority || ''" size="large" />
|
||||||
|
<MessageStatusBadge :status="messageData.status||''" size="large" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<!-- 消息信息 -->
|
||||||
|
<div class="message-info">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="创建时间">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
{{ messageData.createTime }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
{{ messageData.updateTime }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="计划发送时间">
|
||||||
|
<el-icon><Timer /></el-icon>
|
||||||
|
{{ messageData.scheduledTime || '立即发送' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="实际发送时间">
|
||||||
|
<el-icon><Timer /></el-icon>
|
||||||
|
{{ messageData.actualSendTime || '未发送' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="发送方式" :span="2">
|
||||||
|
<el-tag
|
||||||
|
v-for="method in messageData.sendMethods"
|
||||||
|
:key="method"
|
||||||
|
style="margin-right: 8px"
|
||||||
|
>
|
||||||
|
{{ getSendMethodLabel(method) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最大重试次数">
|
||||||
|
{{ messageData.maxRetryCount }} 次
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="当前重试次数">
|
||||||
|
{{ messageData.retryCount || 0 }} 次
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider content-position="left">消息内容</el-divider>
|
||||||
|
|
||||||
|
<!-- 消息内容 -->
|
||||||
|
<div class="message-content">
|
||||||
|
<el-card shadow="never" class="content-card">
|
||||||
|
<div class="content-text">{{ messageData.content }}</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 发送统计(如果有统计数据) -->
|
||||||
|
<template v-if="showStatistics && hasStatistics">
|
||||||
|
<el-divider content-position="left">发送统计</el-divider>
|
||||||
|
<MessageStatistic :statistics="messageData" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作按钮(如果提供) -->
|
||||||
|
<div v-if="showActions" class="message-actions">
|
||||||
|
<slot name="actions" :message="messageData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty v-else-if="!loading" description="消息不存在" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { ArrowLeft, Clock, Timer } from '@element-plus/icons-vue';
|
||||||
|
import { messageApi } from '@/apis/message';
|
||||||
|
import { MessagePriorityBadge, MessageStatusBadge } from '@/components/message';
|
||||||
|
import { MessageStatistic } from '@/views/admin/manage/message/components';
|
||||||
|
import type { MessageVO } from '@/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
messageId: string;
|
||||||
|
showBackButton?: boolean;
|
||||||
|
backButtonText?: string;
|
||||||
|
showStatistics?: boolean;
|
||||||
|
showActions?: boolean;
|
||||||
|
autoMarkRead?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'back'): void;
|
||||||
|
(e: 'loaded', message: MessageVO): void;
|
||||||
|
(e: 'error', error: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
showBackButton: false,
|
||||||
|
backButtonText: '返回',
|
||||||
|
showStatistics: false,
|
||||||
|
showActions: false,
|
||||||
|
autoMarkRead: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const messageData = ref<MessageVO | null>(null);
|
||||||
|
|
||||||
|
/** 是否有统计数据 */
|
||||||
|
const hasStatistics = computed(() => {
|
||||||
|
if (!messageData.value) return false;
|
||||||
|
return (
|
||||||
|
messageData.value.targetUserCount !== undefined &&
|
||||||
|
messageData.value.targetUserCount > 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 加载消息详情 */
|
||||||
|
async function loadMessage() {
|
||||||
|
if (!props.messageId) {
|
||||||
|
ElMessage.warning('消息ID不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await messageApi.getMessageDetail(props.messageId);
|
||||||
|
if (result.success && result.data) {
|
||||||
|
messageData.value = result.data;
|
||||||
|
emit('loaded', result.data);
|
||||||
|
|
||||||
|
// 自动标记已读(用户端)
|
||||||
|
if (props.autoMarkRead) {
|
||||||
|
markAsRead();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '加载失败');
|
||||||
|
emit('error', new Error(result.message || '加载失败'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载消息详情失败:', error);
|
||||||
|
ElMessage.error('加载失败');
|
||||||
|
emit('error', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记已读 */
|
||||||
|
async function markAsRead() {
|
||||||
|
if (!props.messageId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await messageApi.markAsRead(props.messageId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('标记已读失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回 */
|
||||||
|
function handleBack() {
|
||||||
|
emit('back');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取消息类型标签 */
|
||||||
|
function getMessageTypeTag(type: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
notice: '',
|
||||||
|
announcement: 'success',
|
||||||
|
warning: 'warning',
|
||||||
|
system: 'info'
|
||||||
|
};
|
||||||
|
return map[type] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取消息类型文本 */
|
||||||
|
function getMessageTypeLabel(type: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
notice: '通知',
|
||||||
|
announcement: '公告',
|
||||||
|
warning: '警告',
|
||||||
|
system: '系统消息'
|
||||||
|
};
|
||||||
|
return map[type] || type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取发送方式文本 */
|
||||||
|
function getSendMethodLabel(method: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
system: '系统消息',
|
||||||
|
email: '邮件通知',
|
||||||
|
sms: '短信通知'
|
||||||
|
};
|
||||||
|
return map[method] || method;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 刷新数据 */
|
||||||
|
function refresh() {
|
||||||
|
loadMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 监听messageId变化 */
|
||||||
|
watch(() => props.messageId, () => {
|
||||||
|
loadMessage();
|
||||||
|
}, { immediate: false });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadMessage();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
refresh,
|
||||||
|
messageData
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-detail {
|
||||||
|
.detail-container {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #409eff;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-info {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
:deep(.el-descriptions__label) {
|
||||||
|
width: 140px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
background: #f9f9f9;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #606266;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
schoolNewsWeb/src/views/public/message/index.ts
Normal file
7
schoolNewsWeb/src/views/public/message/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @description 公共消息组件导出
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as MessageDetail } from './MessageDetail.vue';
|
||||||
473
schoolNewsWeb/src/views/user/message/MyMessageDetailView.vue
Normal file
473
schoolNewsWeb/src/views/user/message/MyMessageDetailView.vue
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
<template>
|
||||||
|
<div class="my-message-detail" v-loading="loading">
|
||||||
|
<div class="header">
|
||||||
|
<el-button @click="handleBack" text>
|
||||||
|
<el-icon><ArrowLeft /></el-icon>
|
||||||
|
返回消息列表
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="message" class="detail-container">
|
||||||
|
<!-- 消息卡片 -->
|
||||||
|
<el-card class="message-card" shadow="never">
|
||||||
|
<!-- 标题区域 -->
|
||||||
|
<div class="message-header">
|
||||||
|
<div class="title-area">
|
||||||
|
<h1 class="message-title">{{ message.title }}</h1>
|
||||||
|
<div class="title-badges">
|
||||||
|
<el-tag :type="getMessageTypeTag(message.messageType)">
|
||||||
|
{{ getMessageTypeText(message.messageType) }}
|
||||||
|
</el-tag>
|
||||||
|
<MessagePriorityBadge :priority="message.priority || ''" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>发送人:{{ message.senderName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<el-icon><OfficeBuilding /></el-icon>
|
||||||
|
<span>部门:{{ message.senderDeptName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
<span>发送时间:{{ formatDateTime(message.actualSendTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="message.isRead" class="meta-item read-status">
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
<span>已读于:{{ formatDateTime(message.readTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="meta-item unread-status">
|
||||||
|
<el-icon><View /></el-icon>
|
||||||
|
<span>未读</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 消息内容 -->
|
||||||
|
<el-divider />
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="content-body">
|
||||||
|
{{ message.content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 发送方式 -->
|
||||||
|
<el-divider />
|
||||||
|
<div class="send-info">
|
||||||
|
<div class="info-label">
|
||||||
|
<el-icon><Message /></el-icon>
|
||||||
|
<span>发送方式:</span>
|
||||||
|
</div>
|
||||||
|
<div class="send-methods">
|
||||||
|
<el-tag
|
||||||
|
v-if="message.sendMethod === 'system' || !message.sendMethod"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
系统消息
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-if="message.sendMethod === 'email'"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
邮件通知
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-if="message.sendMethod === 'sms'"
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
短信通知
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 发送状态 -->
|
||||||
|
<div class="send-status">
|
||||||
|
<div class="info-label">
|
||||||
|
<el-icon><CircleCheck /></el-icon>
|
||||||
|
<span>发送状态:</span>
|
||||||
|
</div>
|
||||||
|
<el-tag
|
||||||
|
:type="getSendStatusType(message.sendStatus)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getSendStatusText(message.sendStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-if="message.failReason" class="fail-reason">
|
||||||
|
({{ message.failReason }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<el-divider />
|
||||||
|
<div class="message-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="!message.isRead"
|
||||||
|
type="primary"
|
||||||
|
@click="handleMarkAsRead"
|
||||||
|
:loading="marking"
|
||||||
|
>
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
标记为已读
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 相关消息推荐(可选功能) -->
|
||||||
|
<el-card v-if="relatedMessages.length > 0" class="related-messages" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="header-title">相关消息</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="related-list">
|
||||||
|
<div
|
||||||
|
v-for="relMsg in relatedMessages"
|
||||||
|
:key="relMsg.messageID"
|
||||||
|
class="related-item"
|
||||||
|
@click="handleViewRelated(relMsg.messageID)"
|
||||||
|
>
|
||||||
|
<div class="related-title">
|
||||||
|
<span v-if="!relMsg.isRead" class="unread-dot"></span>
|
||||||
|
{{ relMsg.title }}
|
||||||
|
</div>
|
||||||
|
<div class="related-meta">
|
||||||
|
<span>{{ formatDateTime(relMsg.actualSendTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
User,
|
||||||
|
OfficeBuilding,
|
||||||
|
Clock,
|
||||||
|
Check,
|
||||||
|
View,
|
||||||
|
Message,
|
||||||
|
CircleCheck
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import { messageApi } from '@/apis/message';
|
||||||
|
import type { MessageUserVO } from '@/types';
|
||||||
|
import { MessagePriorityBadge } from '@/components/message';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'MyMessageDetailView'
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const messageID = route.params.messageID as string;
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const marking = ref(false);
|
||||||
|
|
||||||
|
const message = ref<MessageUserVO | null>(null);
|
||||||
|
const relatedMessages = ref<MessageUserVO[]>([]);
|
||||||
|
|
||||||
|
/** 获取消息类型标签 */
|
||||||
|
function getMessageTypeTag(type?: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
notice: 'info',
|
||||||
|
announcement: 'warning',
|
||||||
|
warning: 'danger',
|
||||||
|
system: 'success'
|
||||||
|
};
|
||||||
|
return map[type || ''] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取消息类型文本 */
|
||||||
|
function getMessageTypeText(type?: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
notice: '通知',
|
||||||
|
announcement: '公告',
|
||||||
|
warning: '预警',
|
||||||
|
system: '系统消息'
|
||||||
|
};
|
||||||
|
return map[type || ''] || type || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取发送状态类型 */
|
||||||
|
function getSendStatusType(status?: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
pending: 'info',
|
||||||
|
success: 'success',
|
||||||
|
failed: 'danger'
|
||||||
|
};
|
||||||
|
return map[status || ''] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取发送状态文本 */
|
||||||
|
function getSendStatusText(status?: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
pending: '待发送',
|
||||||
|
success: '发送成功',
|
||||||
|
failed: '发送失败'
|
||||||
|
};
|
||||||
|
return map[status || ''] || status || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化日期时间 */
|
||||||
|
function formatDateTime(dateStr?: string): string {
|
||||||
|
if (!dateStr) return '-';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载消息详情 */
|
||||||
|
async function loadMessageDetail() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await messageApi.getMyMessageDetail(messageID);
|
||||||
|
if (result.success) {
|
||||||
|
message.value = result.data || null;
|
||||||
|
|
||||||
|
// 如果是未读消息,自动标记为已读
|
||||||
|
if (message.value && !message.value.isRead) {
|
||||||
|
await autoMarkAsRead();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '加载失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载消息详情失败:', error);
|
||||||
|
ElMessage.error('加载失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 自动标记为已读 */
|
||||||
|
async function autoMarkAsRead() {
|
||||||
|
try {
|
||||||
|
await messageApi.markAsRead(messageID);
|
||||||
|
// 不显示成功消息,静默标记
|
||||||
|
} catch (error) {
|
||||||
|
console.error('自动标记已读失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手动标记为已读 */
|
||||||
|
async function handleMarkAsRead() {
|
||||||
|
marking.value = true;
|
||||||
|
try {
|
||||||
|
const result = await messageApi.markAsRead(messageID);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('已标记为已读');
|
||||||
|
await loadMessageDetail();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('标记已读失败:', error);
|
||||||
|
ElMessage.error('操作失败');
|
||||||
|
} finally {
|
||||||
|
marking.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看相关消息 */
|
||||||
|
function handleViewRelated(relatedMessageID?: string) {
|
||||||
|
if (!relatedMessageID) return;
|
||||||
|
router.push(`/user/message/detail/${relatedMessageID}`);
|
||||||
|
// 重新加载当前页面
|
||||||
|
loadMessageDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回 */
|
||||||
|
function handleBack() {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadMessageDetail();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.my-message-detail {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.message-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.message-header {
|
||||||
|
.title-area {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.message-title {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
&.read-status {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unread-status {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
.content-body {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #333;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-info,
|
||||||
|
.send-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-methods {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fail-reason {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-messages {
|
||||||
|
.card-header {
|
||||||
|
.header-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-list {
|
||||||
|
.related-item {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.unread-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #f56c6c;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
507
schoolNewsWeb/src/views/user/message/MyMessageListView.vue
Normal file
507
schoolNewsWeb/src/views/user/message/MyMessageListView.vue
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
<template>
|
||||||
|
<div class="my-message-list">
|
||||||
|
<div class="header">
|
||||||
|
<h2>我的消息</h2>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-badge :value="unreadCount" :hidden="unreadCount === 0">
|
||||||
|
<el-button @click="loadMessages">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</el-badge>
|
||||||
|
<el-button @click="handleMarkAllRead" :disabled="selectedMessages.length === 0">
|
||||||
|
<el-icon><Check /></el-icon>
|
||||||
|
标记已读
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选条件 -->
|
||||||
|
<el-form :model="filterForm" inline class="filter-form">
|
||||||
|
<el-form-item label="消息类型">
|
||||||
|
<el-select class="w-full" v-model="filterForm.messageType" placeholder="全部" clearable>
|
||||||
|
<el-option label="通知" value="notice" />
|
||||||
|
<el-option label="公告" value="announcement" />
|
||||||
|
<el-option label="预警" value="warning" />
|
||||||
|
<el-option label="系统消息" value="system" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="阅读状态">
|
||||||
|
<el-select class="w-full" v-model="filterForm.isRead" placeholder="全部" clearable>
|
||||||
|
<el-option label="未读" :value="false" />
|
||||||
|
<el-option label="已读" :value="true" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="优先级">
|
||||||
|
<el-select class="w-full" v-model="filterForm.priority" placeholder="全部" clearable>
|
||||||
|
<el-option label="紧急" value="urgent" />
|
||||||
|
<el-option label="重要" value="important" />
|
||||||
|
<el-option label="普通" value="normal" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item class="filter-actions">
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- 消息列表 -->
|
||||||
|
<div class="message-cards" v-loading="loading">
|
||||||
|
<el-checkbox-group v-model="selectedMessages" class="message-group">
|
||||||
|
<div
|
||||||
|
v-for="msg in messageList"
|
||||||
|
:key="msg.messageID"
|
||||||
|
class="message-card"
|
||||||
|
:class="{ unread: !msg.isRead, urgent: msg.priority === 'urgent' }"
|
||||||
|
>
|
||||||
|
<div class="card-header">
|
||||||
|
<el-checkbox :value="msg.messageID" class="message-checkbox" />
|
||||||
|
<div class="card-title-area" @click="handleViewDetail(msg)">
|
||||||
|
<span v-if="!msg.isRead" class="unread-dot"></span>
|
||||||
|
<h3 class="card-title">{{ msg.title }}</h3>
|
||||||
|
<MessagePriorityBadge :priority="msg.priority || ''" />
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="!msg.isRead"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
link
|
||||||
|
@click.stop="handleMarkAsRead(msg.messageID)"
|
||||||
|
>
|
||||||
|
标记已读
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
link
|
||||||
|
@click="handleViewDetail(msg)"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body" @click="handleViewDetail(msg)">
|
||||||
|
<div class="message-meta">
|
||||||
|
<el-tag :type="getMessageTypeTag(msg.messageType)" size="small">
|
||||||
|
{{ getMessageTypeText(msg.messageType) }}
|
||||||
|
</el-tag>
|
||||||
|
<span class="sender-info">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
{{ msg.senderName }} · {{ msg.senderDeptName }}
|
||||||
|
</span>
|
||||||
|
<span class="send-time">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
{{ formatDateTime(msg.actualSendTime) }}
|
||||||
|
</span>
|
||||||
|
<el-tag
|
||||||
|
v-if="msg.isRead"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
已读于 {{ formatDateTime(msg.readTime) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message-preview">
|
||||||
|
{{ getPreviewText(msg.content) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-checkbox-group>
|
||||||
|
|
||||||
|
<el-empty
|
||||||
|
v-if="messageList.length === 0 && !loading"
|
||||||
|
description="暂无消息"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pageParam.pageNumber"
|
||||||
|
v-model:page-size="pageParam.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { Refresh, Check, User, Clock } from '@element-plus/icons-vue';
|
||||||
|
import { messageApi } from '@/apis/message';
|
||||||
|
import type { MessageUserVO, PageParam } from '@/types';
|
||||||
|
import { MessagePriorityBadge } from '@/components/message';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'MyMessageListView'
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const messageList = ref<MessageUserVO[]>([]);
|
||||||
|
const selectedMessages = ref<string[]>([]);
|
||||||
|
const unreadCount = ref(0);
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const pageParam = ref<PageParam>({
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10
|
||||||
|
});
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
// 筛选表单
|
||||||
|
const filterForm = ref<Partial<MessageUserVO>>({
|
||||||
|
messageType: undefined,
|
||||||
|
priority: undefined,
|
||||||
|
isRead: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 获取消息类型标签 */
|
||||||
|
function getMessageTypeTag(type?: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
notice: 'info',
|
||||||
|
announcement: 'warning',
|
||||||
|
warning: 'danger',
|
||||||
|
system: 'success'
|
||||||
|
};
|
||||||
|
return map[type || ''] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取消息类型文本 */
|
||||||
|
function getMessageTypeText(type?: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
notice: '通知',
|
||||||
|
announcement: '公告',
|
||||||
|
warning: '预警',
|
||||||
|
system: '系统'
|
||||||
|
};
|
||||||
|
return map[type || ''] || type || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 格式化日期时间 */
|
||||||
|
function formatDateTime(dateStr?: string): string {
|
||||||
|
if (!dateStr) return '-';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now.getTime() - date.getTime();
|
||||||
|
|
||||||
|
// 1分钟内
|
||||||
|
if (diff < 60000) {
|
||||||
|
return '刚刚';
|
||||||
|
}
|
||||||
|
// 1小时内
|
||||||
|
if (diff < 3600000) {
|
||||||
|
return `${Math.floor(diff / 60000)}分钟前`;
|
||||||
|
}
|
||||||
|
// 24小时内
|
||||||
|
if (diff < 86400000) {
|
||||||
|
return `${Math.floor(diff / 3600000)}小时前`;
|
||||||
|
}
|
||||||
|
// 7天内
|
||||||
|
if (diff < 604800000) {
|
||||||
|
return `${Math.floor(diff / 86400000)}天前`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取预览文本 */
|
||||||
|
function getPreviewText(content?: string): string {
|
||||||
|
if (!content) return '';
|
||||||
|
return content.length > 100 ? content.substring(0, 100) + '...' : content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载消息列表 */
|
||||||
|
async function loadMessages() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await messageApi.getMyMessages(pageParam.value, filterForm.value);
|
||||||
|
if (result.success) {
|
||||||
|
messageList.value = result.dataList || [];
|
||||||
|
total.value = result.pageParam?.totalElements || 0;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '加载失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载消息列表失败:', error);
|
||||||
|
ElMessage.error('加载失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载未读数量 */
|
||||||
|
async function loadUnreadCount() {
|
||||||
|
try {
|
||||||
|
const result = await messageApi.getUnreadCount();
|
||||||
|
if (result.success) {
|
||||||
|
unreadCount.value = result.data || 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载未读数量失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索 */
|
||||||
|
function handleSearch() {
|
||||||
|
pageParam.value.pageNumber = 1;
|
||||||
|
loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置 */
|
||||||
|
function handleReset() {
|
||||||
|
filterForm.value = {
|
||||||
|
isRead: undefined
|
||||||
|
};
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页大小变更 */
|
||||||
|
function handleSizeChange() {
|
||||||
|
pageParam.value.pageNumber = 1;
|
||||||
|
loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 页码变更 */
|
||||||
|
function handlePageChange() {
|
||||||
|
loadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查看详情 */
|
||||||
|
async function handleViewDetail(msg: MessageUserVO) {
|
||||||
|
// 如果是未读消息,先标记为已读
|
||||||
|
if (!msg.isRead && msg.messageID) {
|
||||||
|
await handleMarkAsRead(msg.messageID, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push(`/user/message/detail/${msg.messageID}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记单条消息为已读 */
|
||||||
|
async function handleMarkAsRead(messageID?: string, showMessage = true) {
|
||||||
|
if (!messageID) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await messageApi.markAsRead(messageID);
|
||||||
|
if (result.success) {
|
||||||
|
if (showMessage) {
|
||||||
|
ElMessage.success('已标记为已读');
|
||||||
|
}
|
||||||
|
await loadMessages();
|
||||||
|
await loadUnreadCount();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('标记已读失败:', error);
|
||||||
|
ElMessage.error('操作失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量标记为已读 */
|
||||||
|
async function handleMarkAllRead() {
|
||||||
|
if (selectedMessages.value.length === 0) {
|
||||||
|
ElMessage.warning('请选择要标记的消息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await messageApi.batchMarkAsRead(selectedMessages.value);
|
||||||
|
if (result.success) {
|
||||||
|
ElMessage.success('已标记为已读');
|
||||||
|
selectedMessages.value = [];
|
||||||
|
await loadMessages();
|
||||||
|
await loadUnreadCount();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量标记已读失败:', error);
|
||||||
|
ElMessage.error('操作失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadMessages();
|
||||||
|
loadUnreadCount();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.my-message-list {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px 24px;
|
||||||
|
|
||||||
|
::v-deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-select) {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 88px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-cards {
|
||||||
|
.message-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: #c8232c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unread {
|
||||||
|
border-left: 4px solid #409eff;
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.urgent {
|
||||||
|
border-left: 4px solid #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.message-checkbox {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 4px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.unread-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #f56c6c;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
.message-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
.sender-info,
|
||||||
|
.send-time {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-preview {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
8
schoolNewsWeb/src/views/user/message/index.ts
Normal file
8
schoolNewsWeb/src/views/user/message/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @description 用户消息视图导出
|
||||||
|
* @author Claude
|
||||||
|
* @since 2025-11-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as MyMessageListView } from './MyMessageListView.vue';
|
||||||
|
export { default as MyMessageDetailView } from './MyMessageDetailView.vue';
|
||||||
188
消息通知模块集成说明.md
Normal file
188
消息通知模块集成说明.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# 消息通知模块 - 集成说明
|
||||||
|
|
||||||
|
## 模块概述
|
||||||
|
|
||||||
|
消息通知模块已经完成开发,包含完整的前后端功能。该模块支持系统消息、邮件和短信通知,具有定时发送、重试机制、权限控制等特性。
|
||||||
|
|
||||||
|
## 已完成的文件
|
||||||
|
|
||||||
|
### 后端(Java)
|
||||||
|
- 24个Java文件(DTO、Mapper、Service、Controller、Scheduler、配置类)
|
||||||
|
- 位置:`schoolNewsServ/message/` 和 `schoolNewsServ/api/api-message/`
|
||||||
|
- SQL文件:`schoolNewsServ/.bin/mysql/sql/createTableMessage.sql` 和 `initMenuData.sql`
|
||||||
|
|
||||||
|
### 前端(Vue)
|
||||||
|
1. **类型定义**
|
||||||
|
- `schoolNewsWeb/src/types/message/index.ts`
|
||||||
|
|
||||||
|
2. **API接口**
|
||||||
|
- `schoolNewsWeb/src/apis/message/message.ts`
|
||||||
|
- `schoolNewsWeb/src/apis/message/index.ts`
|
||||||
|
|
||||||
|
3. **通用组件**(4个)
|
||||||
|
- `schoolNewsWeb/src/components/message/MessagePriorityBadge.vue`
|
||||||
|
- `schoolNewsWeb/src/components/message/MessageStatusBadge.vue`
|
||||||
|
- `schoolNewsWeb/src/components/message/MessageSendMethodSelector.vue`
|
||||||
|
- `schoolNewsWeb/src/components/message/MessageTargetSelector.vue` (已删除,功能集成到列表页)
|
||||||
|
- `schoolNewsWeb/src/components/message/index.ts`
|
||||||
|
|
||||||
|
4. **管理端页面**(2个)
|
||||||
|
- `schoolNewsWeb/src/views/admin/manage/message/MessageListView.vue` (集成创建功能)
|
||||||
|
- `schoolNewsWeb/src/views/admin/manage/message/MessageDetailView.vue`
|
||||||
|
- `schoolNewsWeb/src/views/admin/manage/message/index.ts`
|
||||||
|
|
||||||
|
5. **用户端页面**(2个)
|
||||||
|
- `schoolNewsWeb/src/views/user/message/MyMessageListView.vue`
|
||||||
|
- `schoolNewsWeb/src/views/user/message/MyMessageDetailView.vue`
|
||||||
|
- `schoolNewsWeb/src/views/user/message/index.ts`
|
||||||
|
|
||||||
|
## 需要配置的内容
|
||||||
|
|
||||||
|
### 1. 前端路由配置
|
||||||
|
|
||||||
|
在 Vue Router 配置文件中添加以下路由:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 管理端路由
|
||||||
|
{
|
||||||
|
path: '/admin/manage/message',
|
||||||
|
component: AdminLayout, // 或您的布局组件
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'MessageManagement',
|
||||||
|
component: () => import('@/views/admin/manage/message/MessageListView.vue'),
|
||||||
|
meta: { title: '消息管理', requiresAuth: true, permission: 'message:manage' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'detail/:id',
|
||||||
|
name: 'MessageDetail',
|
||||||
|
component: () => import('@/views/admin/manage/message/MessageDetailView.vue'),
|
||||||
|
meta: { title: '消息详情', requiresAuth: true, permission: 'message:manage' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户端路由
|
||||||
|
{
|
||||||
|
path: '/user/message',
|
||||||
|
component: UserLayout, // 或您的用户布局组件
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'MyMessages',
|
||||||
|
component: () => import('@/views/user/message/MyMessageListView.vue'),
|
||||||
|
meta: { title: '我的消息', requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'detail/:id',
|
||||||
|
name: 'MyMessageDetail',
|
||||||
|
component: () => import('@/views/user/message/MyMessageDetailView.vue'),
|
||||||
|
meta: { title: '消息详情', requiresAuth: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 后端配置
|
||||||
|
|
||||||
|
确保以下配置已添加到 `application.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spring:
|
||||||
|
task:
|
||||||
|
scheduling:
|
||||||
|
enabled: true
|
||||||
|
execution:
|
||||||
|
pool:
|
||||||
|
core-size: 10
|
||||||
|
max-size: 20
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 数据库初始化
|
||||||
|
|
||||||
|
执行以下SQL文件:
|
||||||
|
1. `schoolNewsServ/.bin/mysql/sql/createTableMessage.sql` - 创建表结构
|
||||||
|
2. `schoolNewsServ/.bin/mysql/sql/initMenuData.sql` - 初始化菜单和权限数据
|
||||||
|
|
||||||
|
### 4. Maven依赖
|
||||||
|
|
||||||
|
确保父pom.xml中的modules包含:
|
||||||
|
```xml
|
||||||
|
<modules>
|
||||||
|
<module>message</module>
|
||||||
|
</modules>
|
||||||
|
```
|
||||||
|
|
||||||
|
确保api/pom.xml中的modules包含:
|
||||||
|
```xml
|
||||||
|
<modules>
|
||||||
|
<module>api-message</module>
|
||||||
|
</modules>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
### 管理端功能
|
||||||
|
1. **消息列表**
|
||||||
|
- 筛选(标题、类型、状态、优先级)
|
||||||
|
- 分页展示
|
||||||
|
- 创建消息(对话框模式)
|
||||||
|
- 立即发送、重试、改期、取消、删除
|
||||||
|
|
||||||
|
2. **消息详情**
|
||||||
|
- 完整信息展示
|
||||||
|
- 发送统计(进度、成功率、已读率)
|
||||||
|
- 接收对象列表
|
||||||
|
- 用户接收详情
|
||||||
|
|
||||||
|
### 用户端功能
|
||||||
|
1. **我的消息**
|
||||||
|
- 卡片式列表
|
||||||
|
- 未读标识
|
||||||
|
- 批量标记已读
|
||||||
|
- 筛选功能
|
||||||
|
|
||||||
|
2. **消息详情**
|
||||||
|
- 自动标记已读
|
||||||
|
- 精美展示
|
||||||
|
- 响应式设计
|
||||||
|
|
||||||
|
## 权限说明
|
||||||
|
|
||||||
|
数据库中已配置以下权限:
|
||||||
|
- `message:manage` - 消息管理权限(管理端)
|
||||||
|
- `message:send` - 消息发送权限
|
||||||
|
- `message:view` - 消息查看权限(用户端)
|
||||||
|
|
||||||
|
默认绑定到:
|
||||||
|
- superadmin - 所有权限
|
||||||
|
- admin - 管理和发送权限
|
||||||
|
- freedom - 查看权限
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **EmailUtils和SmsUtils**:确保这两个工具类在common模块中存在且可用
|
||||||
|
2. **定时任务**:消息定时发送由`MessageScheduler`处理,每分钟扫描一次
|
||||||
|
3. **权限控制**:用户只能向当前部门及子部门发送消息
|
||||||
|
4. **重试机制**:发送失败后会自动重试,默认最多3次,间隔5分钟
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
1. 先测试立即发送功能
|
||||||
|
2. 测试定时发送功能(设置1-2分钟后的时间)
|
||||||
|
3. 测试权限控制(不同角色的访问)
|
||||||
|
4. 测试用户端的消息查看和已读标记
|
||||||
|
|
||||||
|
## 已知问题
|
||||||
|
|
||||||
|
1. MessageDetailView中的用户接收详情列表需要后端补充接口:`GET /message/users/{messageID}`
|
||||||
|
2. 如果EmailUtils或SmsUtils不存在,需要先实现这两个工具类
|
||||||
|
|
||||||
|
## 后续优化建议
|
||||||
|
|
||||||
|
1. 添加富文本编辑器支持
|
||||||
|
2. 添加消息模板功能
|
||||||
|
3. 添加消息统计报表
|
||||||
|
4. 添加消息分类功能
|
||||||
|
5. 支持附件上传
|
||||||
Reference in New Issue
Block a user