From e20a7755f89e7c60bb8d9cf708c2a2a5b7a76c85 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Thu, 13 Nov 2025 19:00:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=A8=A1=E5=9D=97=E3=80=81?= =?UTF-8?q?=E7=88=AC=E8=99=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.bin/mysql/sql/createTableMessage.sql | 145 ++++ schoolNewsServ/.bin/mysql/sql/initAll.sql | 2 + .../.bin/mysql/sql/initMenuData.sql | 45 +- schoolNewsServ/achievement/docs/MIGRATION.md | 6 +- schoolNewsServ/achievement/pom.xml | 14 +- schoolNewsServ/admin/pom.xml | 31 +- .../admin/src/main/java/org/xyzh/App.java | 3 +- .../src/main/resources/log4j2-spring.xml | 12 +- schoolNewsServ/ai/pom.xml | 10 +- schoolNewsServ/api/api-achievement/pom.xml | 4 +- schoolNewsServ/api/api-ai/pom.xml | 4 +- schoolNewsServ/api/api-all/pom.xml | 8 +- schoolNewsServ/api/api-auth/pom.xml | 4 +- schoolNewsServ/api/api-crontab/pom.xml | 8 +- schoolNewsServ/api/api-file/pom.xml | 4 +- schoolNewsServ/api/api-message/pom.xml | 33 + .../org/xyzh/api/message/MessageService.java | 207 +++++ schoolNewsServ/api/api-news/pom.xml | 4 +- schoolNewsServ/api/api-study/pom.xml | 4 +- schoolNewsServ/api/api-system/pom.xml | 4 +- schoolNewsServ/api/api-usercenter/pom.xml | 4 +- schoolNewsServ/api/pom.xml | 36 +- schoolNewsServ/auth/pom.xml | 18 +- schoolNewsServ/common/common-all/pom.xml | 4 +- .../common/common-annotation/pom.xml | 4 +- schoolNewsServ/common/common-core/pom.xml | 4 +- schoolNewsServ/common/common-dto/pom.xml | 10 +- .../common/dto/message/MessageCreateDTO.java | 95 +++ .../common/dto/message/MessageUserVO.java | 120 +++ .../xyzh/common/dto/message/MessageVO.java | 192 +++++ .../xyzh/common/dto/message/TbSysMessage.java | 148 ++++ .../dto/message/TbSysMessageTarget.java | 64 ++ .../common/dto/message/TbSysMessageUser.java | 68 ++ .../common/common-exception/pom.xml | 4 +- .../src/main/java/org/xyzh/Main.java | 7 - schoolNewsServ/common/common-jdbc/pom.xml | 6 +- schoolNewsServ/common/common-redis/pom.xml | 4 +- schoolNewsServ/common/common-util/pom.xml | 4 +- schoolNewsServ/common/pom.xml | 18 +- schoolNewsServ/crontab/pom.xml | 12 +- schoolNewsServ/file/pom.xml | 8 +- schoolNewsServ/message/pom.xml | 96 +++ .../xyzh/message/config/SchedulingConfig.java | 70 ++ .../message/controller/MessageController.java | 251 ++++++ .../xyzh/message/mapper/MessageMapper.java | 147 ++++ .../message/mapper/MessageTargetMapper.java | 51 ++ .../message/mapper/MessageUserMapper.java | 168 ++++ .../message/scheduler/MessageScheduler.java | 195 +++++ .../service/impl/MessageSendService.java | 319 ++++++++ .../service/impl/MessageServiceImpl.java | 746 ++++++++++++++++++ .../src/main/resources/application.yml | 44 ++ .../main/resources/mapper/MessageMapper.xml | 279 +++++++ .../resources/mapper/MessageTargetMapper.xml | 54 ++ .../resources/mapper/MessageUserMapper.xml | 352 +++++++++ schoolNewsServ/news/pom.xml | 12 +- schoolNewsServ/pom.xml | 74 +- .../sql/add_execute_status_fields.sql | 9 - schoolNewsServ/study/pom.xml | 10 +- schoolNewsServ/system/pom.xml | 8 +- .../org/xyzh/system/mapper/UserMapper.java | 26 +- .../src/main/resources/mapper/UserMapper.xml | 40 +- schoolNewsServ/usercenter/pom.xml | 18 +- schoolNewsWeb/src/apis/ai/agent-config.ts | 2 +- schoolNewsWeb/src/apis/index.ts | 4 +- schoolNewsWeb/src/apis/message/index.ts | 7 + schoolNewsWeb/src/apis/message/message.ts | 216 +++++ .../message/MessagePriorityBadge.vue | 59 ++ .../message/MessageSendMethodSelector.vue | 177 +++++ .../components/message/MessageStatusBadge.vue | 76 ++ .../message/MessageTargetSelector.vue | 649 +++++++++++++++ schoolNewsWeb/src/components/message/index.ts | 10 + schoolNewsWeb/src/types/index.ts | 3 + schoolNewsWeb/src/types/message/index.ts | 141 ++++ schoolNewsWeb/src/utils/route-generator.ts | 12 + .../manage/message/MessageManageView.vue | 737 +++++++++++++++++ .../manage/message/components/MessageAdd.vue | 572 ++++++++++++++ .../message/components/MessageStatistic.vue | 297 +++++++ .../admin/manage/message/components/index.ts | 8 + .../src/views/admin/manage/message/index.ts | 8 + .../views/public/message/MessageDetail.vue | 326 ++++++++ .../src/views/public/message/index.ts | 7 + .../user/message/MyMessageDetailView.vue | 473 +++++++++++ .../views/user/message/MyMessageListView.vue | 507 ++++++++++++ schoolNewsWeb/src/views/user/message/index.ts | 8 + 消息通知模块集成说明.md | 188 +++++ 85 files changed, 8637 insertions(+), 201 deletions(-) create mode 100644 schoolNewsServ/.bin/mysql/sql/createTableMessage.sql create mode 100644 schoolNewsServ/api/api-message/pom.xml create mode 100644 schoolNewsServ/api/api-message/src/main/java/org/xyzh/api/message/MessageService.java create mode 100644 schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageCreateDTO.java create mode 100644 schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageUserVO.java create mode 100644 schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageVO.java create mode 100644 schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessage.java create mode 100644 schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessageTarget.java create mode 100644 schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessageUser.java delete mode 100644 schoolNewsServ/common/common-exception/src/main/java/org/xyzh/Main.java create mode 100644 schoolNewsServ/message/pom.xml create mode 100644 schoolNewsServ/message/src/main/java/org/xyzh/message/config/SchedulingConfig.java create mode 100644 schoolNewsServ/message/src/main/java/org/xyzh/message/controller/MessageController.java create mode 100644 schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageMapper.java create mode 100644 schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageTargetMapper.java create mode 100644 schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageUserMapper.java create mode 100644 schoolNewsServ/message/src/main/java/org/xyzh/message/scheduler/MessageScheduler.java create mode 100644 schoolNewsServ/message/src/main/java/org/xyzh/message/service/impl/MessageSendService.java create mode 100644 schoolNewsServ/message/src/main/java/org/xyzh/message/service/impl/MessageServiceImpl.java create mode 100644 schoolNewsServ/message/src/main/resources/application.yml create mode 100644 schoolNewsServ/message/src/main/resources/mapper/MessageMapper.xml create mode 100644 schoolNewsServ/message/src/main/resources/mapper/MessageTargetMapper.xml create mode 100644 schoolNewsServ/message/src/main/resources/mapper/MessageUserMapper.xml delete mode 100644 schoolNewsServ/sql/add_execute_status_fields.sql create mode 100644 schoolNewsWeb/src/apis/message/index.ts create mode 100644 schoolNewsWeb/src/apis/message/message.ts create mode 100644 schoolNewsWeb/src/components/message/MessagePriorityBadge.vue create mode 100644 schoolNewsWeb/src/components/message/MessageSendMethodSelector.vue create mode 100644 schoolNewsWeb/src/components/message/MessageStatusBadge.vue create mode 100644 schoolNewsWeb/src/components/message/MessageTargetSelector.vue create mode 100644 schoolNewsWeb/src/components/message/index.ts create mode 100644 schoolNewsWeb/src/types/message/index.ts create mode 100644 schoolNewsWeb/src/views/admin/manage/message/MessageManageView.vue create mode 100644 schoolNewsWeb/src/views/admin/manage/message/components/MessageAdd.vue create mode 100644 schoolNewsWeb/src/views/admin/manage/message/components/MessageStatistic.vue create mode 100644 schoolNewsWeb/src/views/admin/manage/message/components/index.ts create mode 100644 schoolNewsWeb/src/views/admin/manage/message/index.ts create mode 100644 schoolNewsWeb/src/views/public/message/MessageDetail.vue create mode 100644 schoolNewsWeb/src/views/public/message/index.ts create mode 100644 schoolNewsWeb/src/views/user/message/MyMessageDetailView.vue create mode 100644 schoolNewsWeb/src/views/user/message/MyMessageListView.vue create mode 100644 schoolNewsWeb/src/views/user/message/index.ts create mode 100644 消息通知模块集成说明.md diff --git a/schoolNewsServ/.bin/mysql/sql/createTableMessage.sql b/schoolNewsServ/.bin/mysql/sql/createTableMessage.sql new file mode 100644 index 0000000..e082672 --- /dev/null +++ b/schoolNewsServ/.bin/mysql/sql/createTableMessage.sql @@ -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:存储每个用户的消息记录,支持已读/未读状态 +-- ===================================================== diff --git a/schoolNewsServ/.bin/mysql/sql/initAll.sql b/schoolNewsServ/.bin/mysql/sql/initAll.sql index 7735b92..17d64f8 100644 --- a/schoolNewsServ/.bin/mysql/sql/initAll.sql +++ b/schoolNewsServ/.bin/mysql/sql/initAll.sql @@ -39,6 +39,8 @@ SOURCE createTableAchievement.sql; SOURCE createTableCrontab.sql; +SOURCE createTableMessage.sql; + -- ===================================================== -- 插入初始数据 -- ===================================================== diff --git a/schoolNewsServ/.bin/mysql/sql/initMenuData.sql b/schoolNewsServ/.bin/mysql/sql/initMenuData.sql index 12c4e63..c6d30cd 100644 --- a/schoolNewsServ/.bin/mysql/sql/initMenuData.sql +++ b/schoolNewsServ/.bin/mysql/sql/initMenuData.sql @@ -33,17 +33,18 @@ INSERT INTO `tb_sys_user_dept_role` (id, user_id, dept_id, role_id, creator, cre ('1', '1', 'root_department', 'superadmin', '1', now()); -- 插入模块数据 -INSERT INTO `tb_sys_module` (id, module_id, name, code, description, icon, order_num, status, creator, create_time) VALUES +INSERT INTO `tb_sys_module` (id, module_id, name, code, description, icon, order_num, status, creator, create_time) VALUES ('1', 'module_system', '系统管理', 'system', '系统管理模块', 'el-icon-setting', 1, 1, '1', now()), ('2', 'module_news', '新闻管理', 'news', '新闻管理模块', 'el-icon-document', 2, 1, '1', now()), ('3', 'module_study', '学习管理', 'study', '学习管理模块', 'el-icon-reading', 3, 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()), ('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 ('0','perm_default', '默认权限', 'default', '默认权限', 'module_system', '1', now()), ('1','perm_system_manage', '系统管理', 'system:manage', '系统管理权限', 'module_system', '1', now()), ('2','perm_system_dept_manage', '系统部门查看', 'system:dept:manage', '系统部门查看权限', 'module_system', '1', now()), @@ -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()), ('13','perm_file_manage', '文件管理', 'file:manage', '文件管理权限', 'module_file', '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 @@ -82,6 +86,9 @@ INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, creat ('15', 'superadmin', 'perm_file_manage', '1', now()), ('16', 'superadmin', 'perm_crontab_manage', '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()), @@ -92,13 +99,17 @@ INSERT INTO `tb_sys_role_permission` (id, role_id, permission_id, creator, creat ('25', 'admin', 'perm_ai_manage', '1', now()), ('26', 'admin', 'perm_usercenter_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()), ('31', 'freedom', 'perm_news_article_add', '1', now()), ('32', 'freedom', 'perm_ai_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 @@ -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), ('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), -('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), ('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), @@ -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), ('501', 'menu_personal_info', '个人信息', 'menu_profile', '/profile/personal-info', 'user/profile/PersonalInfoView', NULL, 1, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), ('502', 'menu_account_settings', '账号设置', 'menu_profile', '/profile/account-settings', 'user/profile/AccountSettingsView', NULL, 2, 0, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), -('600', 'menu_ai_assistant', '智能体模块', NULL, '/ai-assistant', 'user/ai-assistant/AIAssistantView', NULL, 6, 1, 'NavigationLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), -- 管理后台菜单 (1000-8999) ('1000', 'menu_admin_overview', '系统总览', NULL, '/admin/overview', 'admin/overview/SystemOverviewView', 'admin/overview.svg', 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:32', NULL, 0), ('2000', 'menu_sys_manage', '系统管理', NULL, '', '', 'admin/settings.svg', 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:52:35', NULL, 0), @@ -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), ('8001', 'menu_admin_crontab_task', '任务管理', 'menu_admin_crontab_manage', '/admin/manage/crontab/task', 'admin/manage/crontab/TaskManagementView', NULL, 1, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), ('8002', 'menu_admin_crontab_log', '执行日志', 'menu_admin_crontab_manage', '/admin/manage/crontab/log', 'admin/manage/crontab/LogManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0), -('8003', 'menu_admin_news_crawler', '新闻爬虫配置', 'menu_admin_crontab_manage', '/admin/manage/crontab/news-crawler', 'admin/manage/crontab/NewsCrawlerView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0); +('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 -- 前端菜单权限关联 @@ -174,7 +189,6 @@ INSERT INTO `tb_sys_menu_permission` (id, permission_id, menu_id, creator, creat ('115', 'perm_default', 'menu_profile', '1', now()), ('116', 'perm_default', 'menu_personal_info', '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()), ('120', 'perm_news_article_add', 'menu_article_add', '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()), ('233', 'perm_crontab_manage', 'menu_admin_crontab_task', '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()); + diff --git a/schoolNewsServ/achievement/docs/MIGRATION.md b/schoolNewsServ/achievement/docs/MIGRATION.md index e79f18a..ddb811e 100644 --- a/schoolNewsServ/achievement/docs/MIGRATION.md +++ b/schoolNewsServ/achievement/docs/MIGRATION.md @@ -77,7 +77,7 @@ org.xyzh.achievement.controller.AchievementController org.xyzh api-usercenter - ${school-news.version} + 1.0.0 ``` @@ -87,14 +87,14 @@ org.xyzh.achievement.controller.AchievementController org.xyzh api-achievement - ${school-news.version} + 1.0.0 org.xyzh achievement - ${school-news.version} + 1.0.0 ``` diff --git a/schoolNewsServ/achievement/pom.xml b/schoolNewsServ/achievement/pom.xml index bbfc852..844df8f 100644 --- a/schoolNewsServ/achievement/pom.xml +++ b/schoolNewsServ/achievement/pom.xml @@ -6,12 +6,12 @@ org.xyzh school-news - ${school-news.version} + 1.0.0 org.xyzh achievement - ${school-news.version} + 1.0.0 jar achievement 成就模块 @@ -25,28 +25,28 @@ org.xyzh api-achievement - ${school-news.version} + 1.0.0 org.xyzh api-system - ${school-news.version} + 1.0.0 org.xyzh api-study - ${school-news.version} + 1.0.0 org.xyzh common-all - ${school-news.version} + 1.0.0 org.xyzh system - ${school-news.version} + 1.0.0 diff --git a/schoolNewsServ/admin/pom.xml b/schoolNewsServ/admin/pom.xml index ccee69e..e3189b5 100644 --- a/schoolNewsServ/admin/pom.xml +++ b/schoolNewsServ/admin/pom.xml @@ -6,12 +6,12 @@ org.xyzh school-news - ${school-news.version} + 1.0.0 org.xyzh admin - ${school-news.version} + 1.0.0 21 @@ -34,57 +34,62 @@ org.xyzh auth - ${school-news.version} + 1.0.0 org.xyzh common-all - ${school-news.version} + 1.0.0 org.xyzh api-all - ${school-news.version} + 1.0.0 org.xyzh system - ${school-news.version} + 1.0.0 org.xyzh news - ${school-news.version} + 1.0.0 org.xyzh study - ${school-news.version} + 1.0.0 org.xyzh usercenter - ${school-news.version} + 1.0.0 org.xyzh achievement - ${school-news.version} + 1.0.0 org.xyzh file - ${school-news.version} + 1.0.0 org.xyzh ai - ${school-news.version} + 1.0.0 org.xyzh crontab - ${school-news.version} + 1.0.0 + + + org.xyzh + message + 1.0.0 diff --git a/schoolNewsServ/admin/src/main/java/org/xyzh/App.java b/schoolNewsServ/admin/src/main/java/org/xyzh/App.java index c12cee7..f81ba53 100644 --- a/schoolNewsServ/admin/src/main/java/org/xyzh/App.java +++ b/schoolNewsServ/admin/src/main/java/org/xyzh/App.java @@ -17,7 +17,8 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication(scanBasePackages = "org.xyzh") @EnableScheduling @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 static void main(String[] args) { diff --git a/schoolNewsServ/admin/src/main/resources/log4j2-spring.xml b/schoolNewsServ/admin/src/main/resources/log4j2-spring.xml index 8d50c49..1076c41 100644 --- a/schoolNewsServ/admin/src/main/resources/log4j2-spring.xml +++ b/schoolNewsServ/admin/src/main/resources/log4j2-spring.xml @@ -114,7 +114,9 @@ - + + + @@ -173,6 +175,14 @@ + + + + + + + + diff --git a/schoolNewsServ/ai/pom.xml b/schoolNewsServ/ai/pom.xml index f0955fc..e85a1da 100644 --- a/schoolNewsServ/ai/pom.xml +++ b/schoolNewsServ/ai/pom.xml @@ -6,12 +6,12 @@ org.xyzh school-news - ${school-news.version} + 1.0.0 org.xyzh ai - ${school-news.version} + 1.0.0 jar ai 智能体模块 @@ -25,18 +25,18 @@ org.xyzh api-ai - ${school-news.version} + 1.0.0 org.xyzh api-file - ${school-news.version} + 1.0.0 org.xyzh common-all - ${school-news.version} + 1.0.0 org.xyzh diff --git a/schoolNewsServ/api/api-achievement/pom.xml b/schoolNewsServ/api/api-achievement/pom.xml index b57580c..bda23eb 100644 --- a/schoolNewsServ/api/api-achievement/pom.xml +++ b/schoolNewsServ/api/api-achievement/pom.xml @@ -6,12 +6,12 @@ org.xyzh api - ${school-news.version} + 1.0.0 org.xyzh api-achievement - ${school-news.version} + 1.0.0 jar api-achievement 成就模块API接口 diff --git a/schoolNewsServ/api/api-ai/pom.xml b/schoolNewsServ/api/api-ai/pom.xml index 1254bcc..e8344f1 100644 --- a/schoolNewsServ/api/api-ai/pom.xml +++ b/schoolNewsServ/api/api-ai/pom.xml @@ -6,12 +6,12 @@ org.xyzh api - ${school-news.version} + 1.0.0 org.xyzh api-ai - ${school-news.version} + 1.0.0 api-ai 智能体API接口定义 diff --git a/schoolNewsServ/api/api-all/pom.xml b/schoolNewsServ/api/api-all/pom.xml index 2350034..25f148b 100644 --- a/schoolNewsServ/api/api-all/pom.xml +++ b/schoolNewsServ/api/api-all/pom.xml @@ -6,12 +6,12 @@ org.xyzh api - ${school-news.version} + 1.0.0 org.xyzh api-all - ${school-news.version} + 1.0.0 jar @@ -57,5 +57,9 @@ org.xyzh api-crontab + + org.xyzh + api-message + \ No newline at end of file diff --git a/schoolNewsServ/api/api-auth/pom.xml b/schoolNewsServ/api/api-auth/pom.xml index bf3297d..55ba3b2 100644 --- a/schoolNewsServ/api/api-auth/pom.xml +++ b/schoolNewsServ/api/api-auth/pom.xml @@ -6,12 +6,12 @@ org.xyzh api - ${school-news.version} + 1.0.0 org.xyzh api-auth - ${school-news.version} + 1.0.0 21 diff --git a/schoolNewsServ/api/api-crontab/pom.xml b/schoolNewsServ/api/api-crontab/pom.xml index 9eb26f6..94d2e4d 100644 --- a/schoolNewsServ/api/api-crontab/pom.xml +++ b/schoolNewsServ/api/api-crontab/pom.xml @@ -6,12 +6,12 @@ org.xyzh api - ${school-news.version} + 1.0.0 org.xyzh api-crontab - ${school-news.version} + 1.0.0 21 @@ -22,12 +22,12 @@ org.xyzh common-core - ${school-news.version} + 1.0.0 org.xyzh common-dto - ${school-news.version} + 1.0.0 diff --git a/schoolNewsServ/api/api-file/pom.xml b/schoolNewsServ/api/api-file/pom.xml index cd324d2..9932fb2 100644 --- a/schoolNewsServ/api/api-file/pom.xml +++ b/schoolNewsServ/api/api-file/pom.xml @@ -6,12 +6,12 @@ org.xyzh api - ${school-news.version} + 1.0.0 org.xyzh api-file - ${school-news.version} + 1.0.0 jar api-file 文件模块API diff --git a/schoolNewsServ/api/api-message/pom.xml b/schoolNewsServ/api/api-message/pom.xml new file mode 100644 index 0000000..d49d621 --- /dev/null +++ b/schoolNewsServ/api/api-message/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + org.xyzh + api + 1.0.0 + + + org.xyzh + api-message + 1.0.0 + + + 21 + 21 + + + + + org.xyzh + common-core + 1.0.0 + + + org.xyzh + common-dto + 1.0.0 + + + diff --git a/schoolNewsServ/api/api-message/src/main/java/org/xyzh/api/message/MessageService.java b/schoolNewsServ/api/api-message/src/main/java/org/xyzh/api/message/MessageService.java new file mode 100644 index 0000000..060d865 --- /dev/null +++ b/schoolNewsServ/api/api-message/src/main/java/org/xyzh/api/message/MessageService.java @@ -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 创建结果 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain createMessage(TbSysMessage message); + + /** + * 更新消息(仅允许更新草稿状态的消息) + * + * @param message 消息对象 + * @return ResultDomain 更新结果 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain updateMessage(TbSysMessage message); + + /** + * 删除消息(逻辑删除) + * + * @param messageID 消息ID + * @return ResultDomain 删除结果 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain deleteMessage(String messageID); + + /** + * 根据ID查询消息详情 + * + * @param messageID 消息ID + * @return ResultDomain 消息详情 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain getMessageById(String messageID); + + /** + * 分页查询消息列表(管理端) + * + * @param filter 过滤条件 + * @param pageParam 分页参数 + * @return ResultDomain 消息列表 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain getMessagePage(TbSysMessage filter, PageParam pageParam); + + // ================== 消息发送 ================== + + /** + * 发送消息(立即发送或定时发送) + * + * @param messageID 消息ID + * @return ResultDomain 发送结果 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain sendMessage(String messageID); + + /** + * 立即发送(将定时消息改为立即发送) + * + * @param messageID 消息ID + * @return ResultDomain 发送结果 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain sendNow(String messageID); + + /** + * 取消定时消息 + * + * @param messageID 消息ID + * @return ResultDomain 取消结果 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain cancelMessage(String messageID); + + /** + * 修改定时发送时间 + * + * @param messageID 消息ID + * @param scheduledTime 新的发送时间 + * @return ResultDomain 修改结果 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain rescheduleMessage(String messageID, java.util.Date scheduledTime); + + /** + * 重试失败消息 + * + * @param messageID 消息ID + * @return ResultDomain 重试结果 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain retryMessage(String messageID); + + // ================== 用户消息 ================== + + /** + * 分页查询我的消息列表(用户端) + * + * @param filter 过滤条件 + * @param pageParam 分页参数 + * @return ResultDomain 我的消息列表 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain getMyMessagesPage(MessageUserVO filter, PageParam pageParam); + + /** + * 查询我的消息详情(用户端) + * + * @param messageID 消息ID + * @return ResultDomain 消息详情 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain getMyMessageDetail(String messageID); + + /** + * 标记消息为已读 + * + * @param messageID 消息ID + * @return ResultDomain 标记结果 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain markAsRead(String messageID); + + /** + * 批量标记消息为已读 + * + * @param messageIDs 消息ID列表 + * @return ResultDomain 标记成功的数量 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain batchMarkAsRead(List messageIDs); + + /** + * 获取未读消息数量 + * + * @return ResultDomain 未读消息数量 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain getUnreadCount(); + + // ================== 辅助接口 ================== + + /** + * 获取可选的部门树(当前部门及子部门) + * + * @return ResultDomain 部门树数据 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain> getTargetDepts(); + + /** + * 获取可选的角色列表(当前部门及子部门的角色) + * + * @return ResultDomain 角色列表数据 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain> getTargetRoles(); + + /** + * 获取可选的用户列表(当前部门及子部门的用户) + * + * @return ResultDomain 用户列表数据 + * @author Claude + * @since 2025-11-13 + */ + ResultDomain> getTargetUsers(); +} diff --git a/schoolNewsServ/api/api-news/pom.xml b/schoolNewsServ/api/api-news/pom.xml index d58a0fc..c6c3f5c 100644 --- a/schoolNewsServ/api/api-news/pom.xml +++ b/schoolNewsServ/api/api-news/pom.xml @@ -6,12 +6,12 @@ org.xyzh api - ${school-news.version} + 1.0.0 org.xyzh api-news - ${school-news.version} + 1.0.0 jar api-news 新闻API接口定义 diff --git a/schoolNewsServ/api/api-study/pom.xml b/schoolNewsServ/api/api-study/pom.xml index 1ee0bf2..e780c51 100644 --- a/schoolNewsServ/api/api-study/pom.xml +++ b/schoolNewsServ/api/api-study/pom.xml @@ -6,12 +6,12 @@ org.xyzh api - ${school-news.version} + 1.0.0 org.xyzh api-study - ${school-news.version} + 1.0.0 21 diff --git a/schoolNewsServ/api/api-system/pom.xml b/schoolNewsServ/api/api-system/pom.xml index da34c48..b126de6 100644 --- a/schoolNewsServ/api/api-system/pom.xml +++ b/schoolNewsServ/api/api-system/pom.xml @@ -6,12 +6,12 @@ org.xyzh api - ${school-news.version} + 1.0.0 org.xyzh api-system - ${school-news.version} + 1.0.0 21 diff --git a/schoolNewsServ/api/api-usercenter/pom.xml b/schoolNewsServ/api/api-usercenter/pom.xml index d653fea..cfdd749 100644 --- a/schoolNewsServ/api/api-usercenter/pom.xml +++ b/schoolNewsServ/api/api-usercenter/pom.xml @@ -6,12 +6,12 @@ org.xyzh api - ${school-news.version} + 1.0.0 org.xyzh api-usercenter - ${school-news.version} + 1.0.0 api-usercenter 个人中心API接口定义 diff --git a/schoolNewsServ/api/pom.xml b/schoolNewsServ/api/pom.xml index eae8c2a..bad4470 100644 --- a/schoolNewsServ/api/pom.xml +++ b/schoolNewsServ/api/pom.xml @@ -6,12 +6,12 @@ org.xyzh school-news - ${school-news.version} + 1.0.0 org.xyzh api - ${school-news.version} + 1.0.0 pom api-all @@ -24,6 +24,7 @@ api-news api-file api-crontab + api-message @@ -36,57 +37,62 @@ org.xyzh api-all - ${school-news.version} + 1.0.0 org.xyzh api-auth - ${school-news.version} + 1.0.0 org.xyzh api-system - ${school-news.version} + 1.0.0 org.xyzh api-course - ${school-news.version} + 1.0.0 org.xyzh api-study - ${school-news.version} + 1.0.0 org.xyzh api-usercenter - ${school-news.version} + 1.0.0 org.xyzh api-achievement - ${school-news.version} + 1.0.0 org.xyzh api-ai - ${school-news.version} + 1.0.0 org.xyzh api-news - ${school-news.version} + 1.0.0 org.xyzh api-file - ${school-news.version} + 1.0.0 org.xyzh api-crontab - ${school-news.version} + 1.0.0 + + + org.xyzh + api-message + 1.0.0 @@ -94,12 +100,12 @@ org.xyzh common-core - ${school-news.version} + 1.0.0 org.xyzh common-dto - ${school-news.version} + 1.0.0 org.projectlombok diff --git a/schoolNewsServ/auth/pom.xml b/schoolNewsServ/auth/pom.xml index f5ec732..d6695b7 100644 --- a/schoolNewsServ/auth/pom.xml +++ b/schoolNewsServ/auth/pom.xml @@ -6,12 +6,12 @@ org.xyzh school-news - ${school-news.version} + 1.0.0 org.xyzh auth - ${school-news.version} + 1.0.0 jar @@ -24,38 +24,38 @@ org.xyzh common-core - ${school-news.version} + 1.0.0 org.xyzh common-dto - ${school-news.version} + 1.0.0 org.xyzh common-exception - ${school-news.version} + 1.0.0 org.xyzh common-redis - ${school-news.version} + 1.0.0 org.xyzh common-util - ${school-news.version} + 1.0.0 org.xyzh api-auth - ${school-news.version} + 1.0.0 org.xyzh api-system - ${school-news.version} + 1.0.0 diff --git a/schoolNewsServ/common/common-all/pom.xml b/schoolNewsServ/common/common-all/pom.xml index 5fcc7d7..74faaf9 100644 --- a/schoolNewsServ/common/common-all/pom.xml +++ b/schoolNewsServ/common/common-all/pom.xml @@ -6,12 +6,12 @@ org.xyzh common - ${school-news.version} + 1.0.0 org.xyzh common-all - ${school-news.version} + 1.0.0 jar Common All-in-One diff --git a/schoolNewsServ/common/common-annotation/pom.xml b/schoolNewsServ/common/common-annotation/pom.xml index 94135e8..a6442aa 100644 --- a/schoolNewsServ/common/common-annotation/pom.xml +++ b/schoolNewsServ/common/common-annotation/pom.xml @@ -6,12 +6,12 @@ org.xyzh common - ${school-news.version} + 1.0.0 org.xyzh common-annotation - ${school-news.version} + 1.0.0 jar 21 diff --git a/schoolNewsServ/common/common-core/pom.xml b/schoolNewsServ/common/common-core/pom.xml index 9079338..d03d935 100644 --- a/schoolNewsServ/common/common-core/pom.xml +++ b/schoolNewsServ/common/common-core/pom.xml @@ -6,12 +6,12 @@ org.xyzh common - ${school-news.version} + 1.0.0 org.xyzh common-core - ${school-news.version} + 1.0.0 jar 21 diff --git a/schoolNewsServ/common/common-dto/pom.xml b/schoolNewsServ/common/common-dto/pom.xml index 5876d9b..376d922 100644 --- a/schoolNewsServ/common/common-dto/pom.xml +++ b/schoolNewsServ/common/common-dto/pom.xml @@ -6,16 +6,22 @@ org.xyzh common - ${school-news.version} + 1.0.0 org.xyzh common-dto - ${school-news.version} + 1.0.0 jar 21 21 + + + org.projectlombok + lombok + + \ No newline at end of file diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageCreateDTO.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageCreateDTO.java new file mode 100644 index 0000000..07699c6 --- /dev/null +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageCreateDTO.java @@ -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 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; + } +} diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageUserVO.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageUserVO.java new file mode 100644 index 0000000..df3c970 --- /dev/null +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageUserVO.java @@ -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; +} diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageVO.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageVO.java new file mode 100644 index 0000000..17f790f --- /dev/null +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/MessageVO.java @@ -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 targets; + + /** + * 用户接收记录列表(仅在详情查询时返回) + */ + private List 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); + } +} diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessage.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessage.java new file mode 100644 index 0000000..4450808 --- /dev/null +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessage.java @@ -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 targets; + + /** + * 发送方式列表(前端辅助字段,从targets聚合) + */ + private List sendMethods; +} diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessageTarget.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessageTarget.java new file mode 100644 index 0000000..79a11d9 --- /dev/null +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessageTarget.java @@ -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; +} diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessageUser.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessageUser.java new file mode 100644 index 0000000..a045b82 --- /dev/null +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/message/TbSysMessageUser.java @@ -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; +} diff --git a/schoolNewsServ/common/common-exception/pom.xml b/schoolNewsServ/common/common-exception/pom.xml index 4880795..1e7e59b 100644 --- a/schoolNewsServ/common/common-exception/pom.xml +++ b/schoolNewsServ/common/common-exception/pom.xml @@ -6,12 +6,12 @@ org.xyzh common - ${school-news.version} + 1.0.0 org.xyzh common-exception - ${school-news.version} + 1.0.0 jar 21 diff --git a/schoolNewsServ/common/common-exception/src/main/java/org/xyzh/Main.java b/schoolNewsServ/common/common-exception/src/main/java/org/xyzh/Main.java deleted file mode 100644 index f660b7a..0000000 --- a/schoolNewsServ/common/common-exception/src/main/java/org/xyzh/Main.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.xyzh; - -public class Main { - public static void main(String[] args) { - System.out.println("Hello world!"); - } -} \ No newline at end of file diff --git a/schoolNewsServ/common/common-jdbc/pom.xml b/schoolNewsServ/common/common-jdbc/pom.xml index 22fa995..abde219 100644 --- a/schoolNewsServ/common/common-jdbc/pom.xml +++ b/schoolNewsServ/common/common-jdbc/pom.xml @@ -6,12 +6,12 @@ org.xyzh common - ${school-news.version} + 1.0.0 org.xyzh common-jdbc - ${school-news.version} + 1.0.0 jar @@ -36,7 +36,7 @@ org.xyzh common-dto - ${school-news.version} + 1.0.0 diff --git a/schoolNewsServ/common/common-redis/pom.xml b/schoolNewsServ/common/common-redis/pom.xml index 050fb74..1abcbc7 100644 --- a/schoolNewsServ/common/common-redis/pom.xml +++ b/schoolNewsServ/common/common-redis/pom.xml @@ -6,12 +6,12 @@ org.xyzh common - ${school-news.version} + 1.0.0 org.xyzh common-redis - ${school-news.version} + 1.0.0 jar 21 diff --git a/schoolNewsServ/common/common-util/pom.xml b/schoolNewsServ/common/common-util/pom.xml index 271d147..b8d63cd 100644 --- a/schoolNewsServ/common/common-util/pom.xml +++ b/schoolNewsServ/common/common-util/pom.xml @@ -6,12 +6,12 @@ org.xyzh common - ${school-news.version} + 1.0.0 org.xyzh common-util - ${school-news.version} + 1.0.0 jar 21 diff --git a/schoolNewsServ/common/pom.xml b/schoolNewsServ/common/pom.xml index 2a168bb..0fc855f 100644 --- a/schoolNewsServ/common/pom.xml +++ b/schoolNewsServ/common/pom.xml @@ -6,12 +6,12 @@ org.xyzh school-news - ${school-news.version} + 1.0.0 org.xyzh common - ${school-news.version} + 1.0.0 pom common-core @@ -42,37 +42,37 @@ org.xyzh common-core - ${school-news.version} + 1.0.0 org.xyzh common-dto - ${school-news.version} + 1.0.0 org.xyzh common-annotation - ${school-news.version} + 1.0.0 org.xyzh common-exception - ${school-news.version} + 1.0.0 org.xyzh common-redis - ${school-news.version} + 1.0.0 org.xyzh common-jdbc - ${school-news.version} + 1.0.0 org.xyzh common-util - ${school-news.version} + 1.0.0 diff --git a/schoolNewsServ/crontab/pom.xml b/schoolNewsServ/crontab/pom.xml index d11794f..f4e9230 100644 --- a/schoolNewsServ/crontab/pom.xml +++ b/schoolNewsServ/crontab/pom.xml @@ -6,12 +6,12 @@ org.xyzh school-news - ${school-news.version} + 1.0.0 org.xyzh crontab - ${school-news.version} + 1.0.0 21 @@ -23,25 +23,25 @@ org.xyzh api-crontab - ${school-news.version} + 1.0.0 org.xyzh api-news - ${school-news.version} + 1.0.0 org.xyzh common-all - ${school-news.version} + 1.0.0 org.xyzh system - ${school-news.version} + 1.0.0 org.projectlombok diff --git a/schoolNewsServ/file/pom.xml b/schoolNewsServ/file/pom.xml index 29e6be9..375e16b 100644 --- a/schoolNewsServ/file/pom.xml +++ b/schoolNewsServ/file/pom.xml @@ -6,12 +6,12 @@ org.xyzh school-news - ${school-news.version} + 1.0.0 org.xyzh file - ${school-news.version} + 1.0.0 jar file 文件模块 @@ -27,14 +27,14 @@ org.xyzh api-file - ${school-news.version} + 1.0.0 org.xyzh common-all - ${school-news.version} + 1.0.0 diff --git a/schoolNewsServ/message/pom.xml b/schoolNewsServ/message/pom.xml new file mode 100644 index 0000000..f520698 --- /dev/null +++ b/schoolNewsServ/message/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + org.xyzh + school-news + 1.0.0 + + + org.xyzh + message + 1.0.0 + + + 21 + 21 + + + + + + org.xyzh + api-message + 1.0.0 + + + org.xyzh + api-system + 1.0.0 + + + + + org.xyzh + common-all + 1.0.0 + + + + + org.xyzh + system + 1.0.0 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + com.mysql + mysql-connector-j + + + + com.zaxxer + HikariCP + + + + org.springframework.boot + spring-boot-starter-log4j2 + + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + + + diff --git a/schoolNewsServ/message/src/main/java/org/xyzh/message/config/SchedulingConfig.java b/schoolNewsServ/message/src/main/java/org/xyzh/message/config/SchedulingConfig.java new file mode 100644 index 0000000..2432a80 --- /dev/null +++ b/schoolNewsServ/message/src/main/java/org/xyzh/message/config/SchedulingConfig.java @@ -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; + } +} diff --git a/schoolNewsServ/message/src/main/java/org/xyzh/message/controller/MessageController.java b/schoolNewsServ/message/src/main/java/org/xyzh/message/controller/MessageController.java new file mode 100644 index 0000000..b3e0c51 --- /dev/null +++ b/schoolNewsServ/message/src/main/java/org/xyzh/message/controller/MessageController.java @@ -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 + */ + @PostMapping + public ResultDomain createMessage(@RequestBody TbSysMessage message) { + logger.info("创建消息:{}", message.getTitle()); + return messageService.createMessage(message); + } + + /** + * 更新消息(仅草稿状态) + * + * @param message 消息对象 + * @return ResultDomain + */ + @PutMapping + public ResultDomain updateMessage(@RequestBody TbSysMessage message) { + logger.info("更新消息:{}", message.getMessageID()); + return messageService.updateMessage(message); + } + + /** + * 删除消息 + * + * @param messageID 消息ID + * @return ResultDomain + */ + @DeleteMapping + public ResultDomain deleteMessage(@RequestBody TbSysMessage message) { + return messageService.deleteMessage(message.getMessageID()); + } + + /** + * 查询消息详情 + * + * @param messageID 消息ID + * @return ResultDomain + */ + @GetMapping("/detail/{messageID}") + public ResultDomain getMessageById(@PathVariable String messageID) { + return messageService.getMessageById(messageID); + } + + /** + * 分页查询消息列表(管理端) + * + * @param filter 过滤条件 + * @param pageParam 分页参数 + * @return ResultDomain + */ + @PostMapping("/page") + public ResultDomain getMessagePage(@RequestBody TbSysMessage filter, PageParam pageParam) { + return messageService.getMessagePage(filter, pageParam); + } + + + + // ================== 消息发送接口 ================== + + /** + * 发送消息 + * + * @param messageID 消息ID + * @return ResultDomain + */ + @PostMapping("/send/{messageID}") + public ResultDomain sendMessage(@PathVariable String messageID) { + logger.info("发送消息:{}", messageID); + return messageService.sendMessage(messageID); + } + + /** + * 立即发送(定时消息改为立即发送) + * + * @param messageID 消息ID + * @return ResultDomain + */ + @PostMapping("/sendNow/{messageID}") + public ResultDomain sendNow(@PathVariable String messageID) { + logger.info("立即发送消息:{}", messageID); + return messageService.sendNow(messageID); + } + + /** + * 取消定时消息 + * + * @param messageID 消息ID + * @return ResultDomain + */ + @PostMapping("/cancel/{messageID}") + public ResultDomain cancelMessage(@PathVariable String messageID) { + logger.info("取消消息:{}", messageID); + return messageService.cancelMessage(messageID); + } + + /** + * 修改定时发送时间 + * + * @param messageID 消息ID + * @param request 包含scheduledTime的请求对象 + * @return ResultDomain + */ + @PutMapping("/reschedule/{messageID}") + public ResultDomain rescheduleMessage(@PathVariable String messageID, + @RequestBody Map request) { + logger.info("修改消息发送时间:{}", messageID); + Date scheduledTime = (Date) request.get("scheduledTime"); + return messageService.rescheduleMessage(messageID, scheduledTime); + } + + /** + * 重试失败消息 + * + * @param messageID 消息ID + * @return ResultDomain + */ + @PostMapping("/retry/{messageID}") + public ResultDomain retryMessage(@PathVariable String messageID) { + logger.info("重试消息:{}", messageID); + return messageService.retryMessage(messageID); + } + + // ================== 用户消息接口(用户端) ================== + + /** + * 分页查询我的消息列表 + * + * @param filter 过滤条件 + * @param pageParam 分页参数 + * @return ResultDomain + */ + @PostMapping("/my/page") + public ResultDomain getMyMessagesPage(@RequestBody PageRequest pageRequest) { + return messageService.getMyMessagesPage(pageRequest.getFilter(), pageRequest.getPageParam()); + } + + /** + * 查询我的消息详情 + * + * @param messageID 消息ID + * @return ResultDomain + */ + @GetMapping("/my/detail/{messageID}") + public ResultDomain getMyMessageDetail(@PathVariable String messageID) { + return messageService.getMyMessageDetail(messageID); + } + + /** + * 标记消息为已读 + * + * @param messageID 消息ID + * @return ResultDomain + */ + @PostMapping("/my/markRead/{messageID}") + public ResultDomain markAsRead(@PathVariable String messageID) { + return messageService.markAsRead(messageID); + } + + /** + * 批量标记消息为已读 + * + * @param request 包含messageIDs的请求对象 + * @return ResultDomain + */ + @PostMapping("/my/batchMarkRead") + public ResultDomain batchMarkAsRead(@RequestBody Map> request) { + List messageIDs = request.get("messageIDs"); + return messageService.batchMarkAsRead(messageIDs); + } + + /** + * 获取未读消息数量 + * + * @return ResultDomain + */ + @GetMapping("/my/unreadCount") + public ResultDomain getUnreadCount() { + return messageService.getUnreadCount(); + } + + // ================== 辅助接口 ================== + + /** + * 获取可选的部门树 + * + * @return ResultDomain + */ + @GetMapping("/targets/depts") + public ResultDomain> getTargetDepts() { + return messageService.getTargetDepts(); + } + + /** + * 获取可选的角色列表 + * + * @return ResultDomain + */ + @GetMapping("/targets/roles") + public ResultDomain> getTargetRoles() { + return messageService.getTargetRoles(); + } + + /** + * 获取可选的用户列表 + * + * @return ResultDomain + */ + @GetMapping("/targets/users") + public ResultDomain> getTargetUsers() { + return messageService.getTargetUsers(); + } +} diff --git a/schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageMapper.java b/schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageMapper.java new file mode 100644 index 0000000..378698f --- /dev/null +++ b/schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageMapper.java @@ -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 { + + /** + * 根据消息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 消息列表 + * @author Claude + * @since 2025-11-13 + */ + List 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 待发送的消息列表 + * @author Claude + * @since 2025-11-13 + */ + List 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); +} diff --git a/schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageTargetMapper.java b/schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageTargetMapper.java new file mode 100644 index 0000000..7cdf082 --- /dev/null +++ b/schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageTargetMapper.java @@ -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 { + + /** + * 根据消息ID查询接收对象列表 + * + * @param messageID 消息ID + * @return List 接收对象列表 + * @author Claude + * @since 2025-11-13 + */ + List selectByMessageID(@Param("messageID") String messageID); + + /** + * 批量插入接收对象 + * + * @param targets 接收对象列表 + * @return int 插入的行数 + * @author Claude + * @since 2025-11-13 + */ + int batchInsert(@Param("targets") List targets); + + /** + * 根据消息ID删除接收对象 + * + * @param messageID 消息ID + * @return int 删除的行数 + * @author Claude + * @since 2025-11-13 + */ + int deleteByMessageID(@Param("messageID") String messageID); +} diff --git a/schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageUserMapper.java b/schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageUserMapper.java new file mode 100644 index 0000000..a03eea5 --- /dev/null +++ b/schoolNewsServ/message/src/main/java/org/xyzh/message/mapper/MessageUserMapper.java @@ -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 { + + /** + * 根据消息ID查询用户消息列表 + * + * @param messageID 消息ID + * @return List 用户消息列表 + * @author Claude + * @since 2025-11-13 + */ + List selectByMessageID(@Param("messageID") String messageID); + + /** + * 批量插入用户消息 + * + * @param userMessages 用户消息列表 + * @return int 插入的行数 + * @author Claude + * @since 2025-11-13 + */ + int batchInsert(@Param("userMessages") List userMessages); + + /** + * 分页查询当前用户的消息列表 + * + * @param userID 用户ID + * @param filter 过滤条件 + * @return List 消息列表 + * @author Claude + * @since 2025-11-13 + */ + List 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 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 用户消息列表 + * @author Claude + * @since 2025-11-13 + */ + List selectPendingUserMessages(@Param("filter") TbSysMessageUser filter); + + /** + * 动态查询当前用户可见的消息列表(基于 target 配置计算) + * + * @param userID 用户ID + * @param filter 过滤条件 + * @return List 消息列表 + * @author Claude + * @since 2025-11-13 + */ + List 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); +} diff --git a/schoolNewsServ/message/src/main/java/org/xyzh/message/scheduler/MessageScheduler.java b/schoolNewsServ/message/src/main/java/org/xyzh/message/scheduler/MessageScheduler.java new file mode 100644 index 0000000..6e14e5b --- /dev/null +++ b/schoolNewsServ/message/src/main/java/org/xyzh/message/scheduler/MessageScheduler.java @@ -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 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); + } + } +} diff --git a/schoolNewsServ/message/src/main/java/org/xyzh/message/service/impl/MessageSendService.java b/schoolNewsServ/message/src/main/java/org/xyzh/message/service/impl/MessageSendService.java new file mode 100644 index 0000000..822b09f --- /dev/null +++ b/schoolNewsServ/message/src/main/java/org/xyzh/message/service/impl/MessageSendService.java @@ -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 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(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append(""); + html.append("
"); + html.append("
"); + html.append("

").append(message.getTitle()).append("

"); + html.append("
"); + html.append("
"); + html.append(message.getContent()); + html.append("
"); + html.append("
"); + html.append("

发送人:").append(message.getSenderName()).append(" (").append(message.getSenderDeptName()).append(")

"); + html.append("

发送时间:").append(message.getActualSendTime() != null ? message.getActualSendTime().toString() : "").append("

"); + html.append("

此邮件由系统自动发送,请勿回复。

"); + html.append("
"); + html.append("
"); + html.append(""); + html.append(""); + 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); + } + } +} diff --git a/schoolNewsServ/message/src/main/java/org/xyzh/message/service/impl/MessageServiceImpl.java b/schoolNewsServ/message/src/main/java/org/xyzh/message/service/impl/MessageServiceImpl.java new file mode 100644 index 0000000..80a9439 --- /dev/null +++ b/schoolNewsServ/message/src/main/java/org/xyzh/message/service/impl/MessageServiceImpl.java @@ -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 createMessage(TbSysMessage message) { + ResultDomain 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 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 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 updateMessage(TbSysMessage message) { + ResultDomain 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 deleteMessage(String messageID) { + ResultDomain 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 getMessageById(String messageID) { + ResultDomain rt = new ResultDomain<>(); + try { + MessageVO messageVO = messageMapper.selectMessageDetail(messageID); + if (messageVO == null) { + rt.fail("消息不存在"); + return rt; + } + + // 查询接收对象列表 + List targets = messageTargetMapper.selectByMessageID(messageID); + messageVO.setTargets(targets); + + // 查询用户接收记录 + List 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 getMessagePage(TbSysMessage filter, PageParam pageParam) { + ResultDomain rt = new ResultDomain<>(); + try { + if (filter == null) { + filter = new TbSysMessage(); + } + filter.setDeleted(false); + + if (pageParam == null) { + pageParam = new PageParam(); + } + + String currentDeptID = getCurrentUserDeptID(); + + List list = messageMapper.selectMessagePage(filter, currentDeptID); + int total = messageMapper.countMessage(filter, currentDeptID); + + PageDomain 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 sendMessage(String messageID) { + ResultDomain 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 sendNow(String messageID) { + ResultDomain 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 cancelMessage(String messageID) { + ResultDomain 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 rescheduleMessage(String messageID, Date scheduledTime) { + ResultDomain 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 retryMessage(String messageID) { + ResultDomain 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 getMyMessagesPage(MessageUserVO filter, PageParam pageParam) { + ResultDomain rt = new ResultDomain<>(); + try { + if (filter == null) { + filter = new MessageUserVO(); + } + + if (pageParam == null) { + pageParam = new PageParam(); + } + + String currentUserID = getCurrentUserID(); + + // 使用新的动态查询方法 + List list = messageUserMapper.selectMyMessagesWithDynamicTargets(currentUserID, filter); + + PageDomain 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 getMyMessageDetail(String messageID) { + ResultDomain 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 markAsRead(String messageID) { + ResultDomain 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 batchMarkAsRead(List messageIDs) { + ResultDomain 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 getUnreadCount() { + ResultDomain 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> getTargetDepts() { + ResultDomain> rt = new ResultDomain<>(); + // TODO: 实现获取可选部门树 + rt.success("查询成功", new HashMap<>()); + return rt; + } + + @Override + public ResultDomain> getTargetRoles() { + ResultDomain> rt = new ResultDomain<>(); + // TODO: 实现获取可选角色列表 + rt.success("查询成功", new HashMap<>()); + return rt; + } + + @Override + public ResultDomain> getTargetUsers() { + ResultDomain> rt = new ResultDomain<>(); + // TODO: 实现获取可选用户列表 + rt.success("查询成功", new HashMap<>()); + return rt; + } + + // ================== 私有辅助方法 ================== + + /** + * 解析接收对象,生成用户消息列表 + */ + private List resolveTargetUsers(String messageID, List targets, String creator) { + Set userIDSet = new HashSet<>(); + Map userMethodMap = new HashMap<>(); + + for (TbSysMessageTarget target : targets) { + List 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 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"; + } +} diff --git a/schoolNewsServ/message/src/main/resources/application.yml b/schoolNewsServ/message/src/main/resources/application.yml new file mode 100644 index 0000000..c983153 --- /dev/null +++ b/schoolNewsServ/message/src/main/resources/application.yml @@ -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 diff --git a/schoolNewsServ/message/src/main/resources/mapper/MessageMapper.xml b/schoolNewsServ/message/src/main/resources/mapper/MessageMapper.xml new file mode 100644 index 0000000..e8842c0 --- /dev/null +++ b/schoolNewsServ/message/src/main/resources/mapper/MessageMapper.xml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE tb_sys_message + SET status = #{newStatus}, + update_time = NOW() + WHERE message_id = #{messageID} + AND status = #{expectedStatus} + AND deleted = 0 + + + + + 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 tb_sys_message + SET read_count = read_count + 1, + update_time = NOW() + WHERE message_id = #{messageID} + AND deleted = 0 + + + + + + + + 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}) + + + + + UPDATE tb_sys_message + + + title = #{message.title}, + + + content = #{message.content}, + + + message_type = #{message.messageType}, + + + priority = #{message.priority}, + + + send_mode = #{message.sendMode}, + + + scheduled_time = #{message.scheduledTime}, + + + actual_send_time = #{message.actualSendTime}, + + + status = #{message.status}, + + + target_user_count = #{message.targetUserCount}, + + + retry_count = #{message.retryCount}, + + + max_retry_count = #{message.maxRetryCount}, + + + last_error = #{message.lastError}, + + + updater = #{message.updater}, + + + update_time = #{message.updateTime}, + + + WHERE message_id = #{message.messageID} + AND deleted = 0 + + + + + UPDATE tb_sys_message + SET deleted = 1, + delete_time = NOW(), + update_time = NOW() + WHERE message_id = #{messageID} + AND deleted = 0 + + + + + + diff --git a/schoolNewsServ/message/src/main/resources/mapper/MessageTargetMapper.xml b/schoolNewsServ/message/src/main/resources/mapper/MessageTargetMapper.xml new file mode 100644 index 0000000..e54eb86 --- /dev/null +++ b/schoolNewsServ/message/src/main/resources/mapper/MessageTargetMapper.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + (#{item.id}, #{item.messageID}, #{item.sendMethod}, #{item.targetType}, #{item.targetID}, + #{item.targetName}, #{item.scopeDeptID}, #{item.creator}, NOW(), NOW(), 0) + + + + + + UPDATE tb_sys_message_target + SET deleted = 1, + delete_time = NOW(), + update_time = NOW() + WHERE message_id = #{messageID} + AND deleted = 0 + + + diff --git a/schoolNewsServ/message/src/main/resources/mapper/MessageUserMapper.xml b/schoolNewsServ/message/src/main/resources/mapper/MessageUserMapper.xml new file mode 100644 index 0000000..48b65f2 --- /dev/null +++ b/schoolNewsServ/message/src/main/resources/mapper/MessageUserMapper.xml @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO tb_sys_message_user + (id, message_id, user_id, send_method, is_read, send_status, creator, create_time, update_time, deleted) + VALUES + + (#{item.id}, #{item.messageID}, #{item.userID}, #{item.sendMethod}, + 0, 'pending', #{item.creator}, NOW(), NOW(), 0) + + + + + + + + + + + + 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 tb_sys_message_user + SET is_read = 1, + read_time = NOW(), + update_time = NOW() + WHERE user_id = #{userID} + AND message_id IN + + #{id} + + AND deleted = 0 + + + + + + + + + + + UPDATE tb_sys_message_user + SET send_status = #{sendStatus}, + + fail_reason = #{failReason}, + + update_time = NOW() + WHERE id = #{id} + AND deleted = 0 + + + + + + + + + + + + + + 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 + ) + + + diff --git a/schoolNewsServ/news/pom.xml b/schoolNewsServ/news/pom.xml index ae7842d..99d1ab8 100644 --- a/schoolNewsServ/news/pom.xml +++ b/schoolNewsServ/news/pom.xml @@ -6,12 +6,12 @@ org.xyzh school-news - ${school-news.version} + 1.0.0 org.xyzh news - ${school-news.version} + 1.0.0 21 @@ -22,22 +22,22 @@ org.xyzh api-news - ${school-news.version} + 1.0.0 org.xyzh api-usercenter - ${school-news.version} + 1.0.0 org.xyzh system - ${school-news.version} + 1.0.0 org.xyzh common-all - ${school-news.version} + 1.0.0 diff --git a/schoolNewsServ/pom.xml b/schoolNewsServ/pom.xml index e7c4460..d6919b6 100644 --- a/schoolNewsServ/pom.xml +++ b/schoolNewsServ/pom.xml @@ -6,7 +6,7 @@ org.xyzh school-news pom - ${school-news.version} + 1.0.0 api @@ -21,6 +21,7 @@ ai file crontab + message @@ -57,7 +58,7 @@ 5.2.3 - + 1.18.40 @@ -67,63 +68,68 @@ org.xyzh api - ${school-news.version} + 1.0.0 org.xyzh common - ${school-news.version} + 1.0.0 org.xyzh common-all - ${school-news.version} + 1.0.0 org.xyzh auth - ${school-news.version} + 1.0.0 org.xyzh system - ${school-news.version} + 1.0.0 org.xyzh news - ${school-news.version} + 1.0.0 org.xyzh course - ${school-news.version} + 1.0.0 org.xyzh study - ${school-news.version} + 1.0.0 + + + org.xyzh + usercenter + 1.0.0 + + + org.xyzh + achievement + 1.0.0 + + + org.xyzh + ai + 1.0.0 + + + org.xyzh + crontab + 1.0.0 + + + org.xyzh + message + 1.0.0 - - org.xyzh - usercenter - ${school-news.version} - - - org.xyzh - achievement - ${school-news.version} - - - org.xyzh - ai - ${school-news.version} - - - org.xyzh - crontab - ${school-news.version} - @@ -273,11 +279,11 @@ poi ${poi.version} - + - org.apache.poi - poi-ooxml - ${poi.version} + org.apache.poi + poi-ooxml + ${poi.version} diff --git a/schoolNewsServ/sql/add_execute_status_fields.sql b/schoolNewsServ/sql/add_execute_status_fields.sql deleted file mode 100644 index f0b5b5b..0000000 --- a/schoolNewsServ/sql/add_execute_status_fields.sql +++ /dev/null @@ -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; diff --git a/schoolNewsServ/study/pom.xml b/schoolNewsServ/study/pom.xml index e5eec7e..b81a823 100644 --- a/schoolNewsServ/study/pom.xml +++ b/schoolNewsServ/study/pom.xml @@ -6,12 +6,12 @@ org.xyzh school-news - ${school-news.version} + 1.0.0 org.xyzh study - ${school-news.version} + 1.0.0 jar study 学习管理模块 @@ -25,17 +25,17 @@ org.xyzh api-study - ${school-news.version} + 1.0.0 org.xyzh system - ${school-news.version} + 1.0.0 org.xyzh common-all - ${school-news.version} + 1.0.0 diff --git a/schoolNewsServ/system/pom.xml b/schoolNewsServ/system/pom.xml index 0a3dec1..c09f407 100644 --- a/schoolNewsServ/system/pom.xml +++ b/schoolNewsServ/system/pom.xml @@ -6,12 +6,12 @@ org.xyzh school-news - ${school-news.version} + 1.0.0 org.xyzh system - ${school-news.version} + 1.0.0 21 @@ -23,14 +23,14 @@ org.xyzh api-system - ${school-news.version} + 1.0.0 org.xyzh common-all - ${school-news.version} + 1.0.0 diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java index 100ac02..9f2c462 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java @@ -50,6 +50,15 @@ public interface UserMapper extends BaseMapper { */ 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 根据用户名查询用户 * @param username 用户名 @@ -182,6 +191,21 @@ public interface UserMapper extends BaseMapper { */ UserVO selectUserInfoTotal(@Param("userId") String userId); - + int countDeptUser(@Param("deptId") String deptId); + + /** + * @description 查询部门及其子部门的所有用户ID + * @param deptId 部门ID + * @return List 用户ID列表 + */ + List selectUserIdsByDeptId(@Param("deptId") String deptId); + + /** + * @description 查询指定部门及其子部门中指定角色的所有用户ID + * @param deptId 部门ID + * @param roleId 角色ID + * @return List 用户ID列表 + */ + List selectUserIdsByDeptRole(@Param("deptId") String deptId, @Param("roleId") String roleId); } diff --git a/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml b/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml index a2a1058..be0f059 100644 --- a/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml +++ b/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml @@ -143,6 +143,15 @@ + + + - + + + + + + +