浏览代码

就业援助、政策推送和岗位信息变动推送、就业状态自动感知和信息智能推送模块第一版

kk 6 天之前
父节点
当前提交
a6652ba874
共有 21 个文件被更改,包括 1267 次插入0 次删除
  1. 253 0
      .docs/sql/信息智能推送模块-补充字典数据.sql
  2. 101 0
      .docs/sql/修复见习岗位和公益性岗位导出按钮权限.sql
  3. 29 0
      .docs/sql/消息通知记录表.sql
  4. 二进制
      doc/~$市人力资源和社会保障局智慧人社运营运维(2025-2027年)项目需求规格说明书-就业一湛通服务平台.docx
  5. 326 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/controller/NotificationController.java
  6. 63 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/entity/NotificationRecord.java
  7. 33 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/entity/NotificationTarget.java
  8. 7 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/mapper/NotificationRecordMapper.java
  9. 7 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/mapper/NotificationTargetMapper.java
  10. 4 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/mapper/xml/NotificationRecordMapper.xml
  11. 7 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/service/INotificationRecordService.java
  12. 11 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/service/impl/NotificationRecordServiceImpl.java
  13. 23 0
      jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260611_1__create_notification_record.sql
  14. 12 0
      jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260611_2__menu_insert_SentMessages.sql
  15. 12 0
      jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260611_3__menu_insert_AllMessages.sql
  16. 5 0
      jeecgboot-vue3/src/views/notification/AllMessages.api.ts
  17. 10 0
      jeecgboot-vue3/src/views/notification/AllMessages.data.ts
  18. 175 0
      jeecgboot-vue3/src/views/notification/AllMessagesList.vue
  19. 5 0
      jeecgboot-vue3/src/views/notification/SentMessages.api.ts
  20. 9 0
      jeecgboot-vue3/src/views/notification/SentMessages.data.ts
  21. 175 0
      jeecgboot-vue3/src/views/notification/SentMessagesList.vue

+ 253 - 0
.docs/sql/信息智能推送模块-补充字典数据.sql

@@ -0,0 +1,253 @@
+-- ============================================================
+-- 信息智能匹配推送模块 - 补充字典数据
+--
+-- 说明:这些字典在 Flyway MySQL 脚本(V20260605_3)中已插入
+--       sys_dict/sys_dict_item,但前端 useDict 从 DICTIONARY/
+--       DICTIONARY_ITEM 表加载,需在此表中补充
+--
+-- DICTIONARY_ITEM 结构:
+--   DictionaryItemID, Code, DictionaryCode, Value(INT), Name, OrderNo, RecordStatus, IsEditable, ParentItemID
+--
+-- Code 字段规则:
+--   - DB存数值的字典(如 publishStatus='0'/'1'):Code='',Value 存匹配的 INT
+--   - DB存文本的字典(如 companyNature='国有企业'):Code 存文本,Value 存序号
+--
+-- 数据库:达梦数据库 (DM8)
+-- 创建日期:2026-06-11
+-- ============================================================
+
+-- ============================================================
+-- 1. 发布状态 (publish_status) — 见习岗位/公益性岗位共用
+-- DB存储: '0'=未发布, '1'=已发布
+-- ============================================================
+INSERT INTO DICTIONARY (DictionaryCode, Name, OrderNo, RecordStatus, DictType)
+SELECT 'publish_status', '发布状态', 101, 1, 0 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY WHERE DictionaryCode = 'publish_status');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000001', '', 'publish_status', 0, '未发布', 1, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'publish_status' AND Value = 0);
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000002', '', 'publish_status', 1, '已发布', 2, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'publish_status' AND Value = 1);
+
+-- ============================================================
+-- 2. 报名状态 (apply_status) — 见习岗位
+-- DB存储: '01'=报名中, '02'=人员已满(前导零,需用Code存储)
+-- ============================================================
+INSERT INTO DICTIONARY (DictionaryCode, Name, OrderNo, RecordStatus, DictType)
+SELECT 'apply_status', '报名状态', 102, 1, 0 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY WHERE DictionaryCode = 'apply_status');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000003', '01', 'apply_status', 1, '报名中', 1, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'apply_status' AND Code = '01');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000004', '02', 'apply_status', 2, '人员已满', 2, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'apply_status' AND Code = '02');
+
+-- ============================================================
+-- 3. 单位性质 (company_nature) — 见习岗位
+-- DB存储文本: '国有企业','民营企业','外资企业','事业单位','社会团体'
+-- ============================================================
+INSERT INTO DICTIONARY (DictionaryCode, Name, OrderNo, RecordStatus, DictType)
+SELECT 'company_nature', '单位性质', 103, 1, 0 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY WHERE DictionaryCode = 'company_nature');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000005', '国有企业', 'company_nature', 1, '国有企业', 1, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'company_nature' AND Code = '国有企业');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000006', '民营企业', 'company_nature', 2, '民营企业', 2, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'company_nature' AND Code = '民营企业');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000007', '外资企业', 'company_nature', 3, '外资企业', 3, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'company_nature' AND Code = '外资企业');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000008', '事业单位', 'company_nature', 4, '事业单位', 4, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'company_nature' AND Code = '事业单位');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000009', '社会团体', 'company_nature', 5, '社会团体', 5, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'company_nature' AND Code = '社会团体');
+
+-- ============================================================
+-- 4. 公益性岗位类型 (welfare_post_type) — 公益性岗位
+-- DB存储文本: '社区服务','城市管理','公共管理','后勤服务'
+-- ============================================================
+INSERT INTO DICTIONARY (DictionaryCode, Name, OrderNo, RecordStatus, DictType)
+SELECT 'welfare_post_type', '公益性岗位类型', 104, 1, 0 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY WHERE DictionaryCode = 'welfare_post_type');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000010', '社区服务', 'welfare_post_type', 1, '社区服务', 1, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'welfare_post_type' AND Code = '社区服务');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000011', '城市管理', 'welfare_post_type', 2, '城市管理', 2, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'welfare_post_type' AND Code = '城市管理');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000012', '公共管理', 'welfare_post_type', 3, '公共管理', 3, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'welfare_post_type' AND Code = '公共管理');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000013', '后勤服务', 'welfare_post_type', 4, '后勤服务', 4, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'welfare_post_type' AND Code = '后勤服务');
+
+-- ============================================================
+-- 5. 公益性岗位状态 (welfare_post_status) — 公益性岗位
+-- DB存储文本: '招聘中','已满','已关闭'
+-- ============================================================
+INSERT INTO DICTIONARY (DictionaryCode, Name, OrderNo, RecordStatus, DictType)
+SELECT 'welfare_post_status', '公益性岗位状态', 105, 1, 0 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY WHERE DictionaryCode = 'welfare_post_status');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000014', '招聘中', 'welfare_post_status', 1, '招聘中', 1, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'welfare_post_status' AND Code = '招聘中');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000015', '已满', 'welfare_post_status', 2, '已满', 2, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'welfare_post_status' AND Code = '已满');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000016', '已关闭', 'welfare_post_status', 3, '已关闭', 3, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'welfare_post_status' AND Code = '已关闭');
+
+-- ============================================================
+-- 6. 推荐类型 (recommend_type) — 岗位推荐
+-- DB存储: '1'=岗位推荐到人, '2'=人推荐到岗位
+-- ============================================================
+INSERT INTO DICTIONARY (DictionaryCode, Name, OrderNo, RecordStatus, DictType)
+SELECT 'recommend_type', '推荐类型', 106, 1, 0 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY WHERE DictionaryCode = 'recommend_type');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000017', '', 'recommend_type', 1, '岗位推荐到人', 1, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'recommend_type' AND Value = 1);
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000018', '', 'recommend_type', 2, '人推荐到岗位', 2, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'recommend_type' AND Value = 2);
+
+-- ============================================================
+-- 7. 推荐状态 (recommend_status) — 岗位推荐
+-- DB存储: '0'=待查看, '1'=已查看, '2'=已接受, '3'=已拒绝
+-- ============================================================
+INSERT INTO DICTIONARY (DictionaryCode, Name, OrderNo, RecordStatus, DictType)
+SELECT 'recommend_status', '推荐状态', 107, 1, 0 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY WHERE DictionaryCode = 'recommend_status');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000019', '', 'recommend_status', 0, '待查看', 1, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'recommend_status' AND Value = 0);
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000020', '', 'recommend_status', 1, '已查看', 2, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'recommend_status' AND Value = 1);
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000021', '', 'recommend_status', 2, '已接受', 3, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'recommend_status' AND Value = 2);
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000022', '', 'recommend_status', 3, '已拒绝', 4, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'recommend_status' AND Value = 3);
+
+-- ============================================================
+-- 8. 见习状态 (internship_status) — 见习人员
+-- DB存储文本: '见习中','已期满','已退出'
+-- ============================================================
+INSERT INTO DICTIONARY (DictionaryCode, Name, OrderNo, RecordStatus, DictType)
+SELECT 'internship_status', '见习状态', 108, 1, 0 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY WHERE DictionaryCode = 'internship_status');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000023', '见习中', 'internship_status', 1, '见习中', 1, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'internship_status' AND Code = '见习中');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000024', '已期满', 'internship_status', 2, '已期满', 2, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'internship_status' AND Code = '已期满');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000025', '已退出', 'internship_status', 3, '已退出', 3, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'internship_status' AND Code = '已退出');
+
+-- ============================================================
+-- 9. 审核状态 (audit_status) — 见习人员
+-- DB存储文本: '待审核','已通过','未通过'
+-- ============================================================
+INSERT INTO DICTIONARY (DictionaryCode, Name, OrderNo, RecordStatus, DictType)
+SELECT 'audit_status', '审核状态', 109, 1, 0 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY WHERE DictionaryCode = 'audit_status');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000026', '待审核', 'audit_status', 1, '待审核', 1, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'audit_status' AND Code = '待审核');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000027', '已通过', 'audit_status', 2, '已通过', 2, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'audit_status' AND Code = '已通过');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000028', '未通过', 'audit_status', 3, '未通过', 3, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'audit_status' AND Code = '未通过');
+
+-- ============================================================
+-- 验证
+-- ============================================================
+SELECT '字典主表' AS 检查项, DictionaryCode, Name FROM DICTIONARY
+WHERE DictionaryCode IN ('publish_status','apply_status','company_nature','welfare_post_type','welfare_post_status','recommend_type','recommend_status','internship_status','audit_status')
+ORDER BY DictionaryCode;
+
+SELECT '字典项' AS 检查项, DictionaryCode, Code, Value, Name, OrderNo FROM DICTIONARY_ITEM
+WHERE DictionaryCode IN ('publish_status','apply_status','company_nature','welfare_post_type','welfare_post_status','recommend_type','recommend_status','internship_status','audit_status')
+ORDER BY DictionaryCode, OrderNo;
+
+-- 注意:Education 和 Gender 字典的 Code 字段保持为空('')
+-- 因为 DB 存储的是数值('1','2','3'),匹配 DICTIONARY_ITEM.Value(INT)
+-- getDictText 通过 String(Value)===String(data) 匹配,无需 Code
+-- getDictOptions 返回 value=String(Value),a-select v-model 数值与 DB 一致
+
+-- ============================================================
+-- 10. 身份要求 (identity_requirement) — 见习岗位
+-- DB存储文本: '高校毕业生','失业青年','脱贫家庭青年'
+-- ============================================================
+INSERT INTO DICTIONARY (DictionaryCode, Name, OrderNo, RecordStatus, DictType)
+SELECT 'identity_requirement', '身份要求', 110, 1, 0 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY WHERE DictionaryCode = 'identity_requirement');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000029', '高校毕业生', 'identity_requirement', 1, '高校毕业生', 1, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'identity_requirement' AND Value = 1);
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000030', '失业青年', 'identity_requirement', 2, '失业青年', 2, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'identity_requirement' AND Value = 2);
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000031', '脱贫家庭青年', 'identity_requirement', 3, '脱贫家庭青年', 3, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'identity_requirement' AND Value = 3);
+
+-- ============================================================
+-- 11. 报名方式 (apply_method) — 见习岗位
+-- DB存储文本: '网上报名','现场报名'
+-- ============================================================
+INSERT INTO DICTIONARY (DictionaryCode, Name, OrderNo, RecordStatus, DictType)
+SELECT 'apply_method', '报名方式', 111, 1, 0 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY WHERE DictionaryCode = 'apply_method');
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000032', '网上报名', 'apply_method', 1, '网上报名', 1, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'apply_method' AND Value = 1);
+
+INSERT INTO DICTIONARY_ITEM (DictionaryItemID, Code, DictionaryCode, Value, Name, OrderNo, RecordStatus, IsEditable)
+SELECT '178060600000033', '现场报名', 'apply_method', 2, '现场报名', 2, 1, 1 FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM DICTIONARY_ITEM WHERE DictionaryCode = 'apply_method' AND Value = 2);

+ 101 - 0
.docs/sql/修复见习岗位和公益性岗位导出按钮权限.sql

@@ -0,0 +1,101 @@
+-- ============================================================
+-- 湛江市智慧人社运营运维项目
+-- 修复见习岗位管理和公益性岗位管理导出按钮不显示问题
+--
+-- 根本原因:
+--   后端 SysPermissionController 构建 codeList 时过滤 status='1'
+--   但 V20260603_5/V20260603_7 Flyway脚本插入的按钮权限 status=NULL
+--   导致权限码不被返回,前端 v-auth 指令隐藏导出按钮
+--
+-- 修复内容:
+--   1. 修复 internship_post 和 welfare_post 所有按钮权限 status=1
+--   2. 确保导出权限已授权给admin角色
+--
+-- 数据库:达梦数据库 (DM8) 兼容
+-- 创建日期:2026-06-11
+-- ============================================================
+
+-- ============================================================
+-- 第一部分:修复见习岗位管理按钮权限 status
+-- ============================================================
+UPDATE sys_permission
+SET status = 1
+WHERE perms IN (
+    'internship_post:add',
+    'internship_post:edit',
+    'internship_post:delete',
+    'internship_post:deleteBatch',
+    'internship_post:exportXls',
+    'internship_post:importExcel',
+    'internship_post:publish',
+    'internship_post:unpublish',
+    'internship_post:updateApplyStatus',
+    'internship_post:copy'
+)
+  AND (status IS NULL OR status != 1);
+
+-- ============================================================
+-- 第二部分:修复公益性岗位管理按钮权限 status
+-- ============================================================
+UPDATE sys_permission
+SET status = 1
+WHERE perms IN (
+    'welfare_post:add',
+    'welfare_post:edit',
+    'welfare_post:delete',
+    'welfare_post:deleteBatch',
+    'welfare_post:exportXls',
+    'welfare_post:importExcel',
+    'welfare_post:publish',
+    'welfare_post:unpublish',
+    'welfare_post:updatePostStatus'
+)
+  AND (status IS NULL OR status != 1);
+
+-- ============================================================
+-- 第三部分:确保导出权限已授权给admin角色
+-- admin角色ID: f6817f48af4fb3af11b9e8bf182f618b
+-- ============================================================
+
+-- 3.1 见习岗位导出权限授权(权限ID: 1780601000000315)
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip)
+SELECT '1780601000000322', 'f6817f48af4fb3af11b9e8bf182f618b', '1780601000000315', NULL, '2026-06-11 10:00:00', '127.0.0.1'
+FROM DUAL
+WHERE NOT EXISTS (
+    SELECT 1 FROM sys_role_permission
+    WHERE role_id = 'f6817f48af4fb3af11b9e8bf182f618b'
+      AND permission_id = '1780601000000315'
+);
+
+-- 3.2 公益性岗位导出权限授权(权限ID: 1780601000000415)
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip)
+SELECT '1780601000000422', 'f6817f48af4fb3af11b9e8bf182f618b', '1780601000000415', NULL, '2026-06-11 10:00:00', '127.0.0.1'
+FROM DUAL
+WHERE NOT EXISTS (
+    SELECT 1 FROM sys_role_permission
+    WHERE role_id = 'f6817f48af4fb3af11b9e8bf182f618b'
+      AND permission_id = '1780601000000415'
+);
+
+-- ============================================================
+-- 第四部分:验证修复结果
+-- ============================================================
+
+-- 4.1 检查按钮权限status是否已修复
+SELECT '见习岗位权限status' AS 检查项, id, name, perms, status
+FROM sys_permission
+WHERE perms LIKE 'internship_post:%' AND menu_type = 2
+ORDER BY perms;
+
+-- 4.2 检查公益性岗位权限status
+SELECT '公益性岗位权限status' AS 检查项, id, name, perms, status
+FROM sys_permission
+WHERE perms LIKE 'welfare_post:%' AND menu_type = 2
+ORDER BY perms;
+
+-- 4.3 检查导出权限角色授权
+SELECT '角色授权检查' AS 检查项, p.perms, rp.role_id
+FROM sys_permission p
+LEFT JOIN sys_role_permission rp ON rp.permission_id = p.id
+  AND rp.role_id = 'f6817f48af4fb3af11b9e8bf182f618b'
+WHERE p.perms IN ('internship_post:exportXls', 'welfare_post:exportXls');

+ 29 - 0
.docs/sql/消息通知记录表.sql

@@ -0,0 +1,29 @@
+-- ============================================================
+-- 消息通知记录表 + 接收人关联表(DM8)
+-- 设计:消息主题/内容只存一份,接收人独立关联,批量发送无冗余
+-- 接收人名称不存储,查询时关联 PERSONAL_INFO/ENTERPRISE_INFO 实时获取
+-- ============================================================
+
+CREATE TABLE notification_record (
+    ID            VARCHAR(36)  NOT NULL,
+    MODULE_TYPE   VARCHAR(50)           COMMENT '来源模块',
+    SUBJECT       VARCHAR(200) NOT NULL COMMENT '消息主题',
+    CONTENT       VARCHAR(2000)         COMMENT '消息内容',
+    SENDER_ID     VARCHAR(36)           COMMENT '推送人用户ID(关联sys_user)',
+    SENDER        VARCHAR(50)  NOT NULL COMMENT '推送人姓名',
+    SEND_TIME     TIMESTAMP    NOT NULL COMMENT '推送时间',
+    CREATE_BY     VARCHAR(50),
+    CREATE_TIME   TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
+    SYS_ORG_CODE  VARCHAR(50),
+    PRIMARY KEY (ID)
+);
+COMMENT ON TABLE notification_record IS '消息通知记录表';
+
+CREATE TABLE notification_target (
+    NOTIFICATION_ID VARCHAR(36) NOT NULL COMMENT '消息ID',
+    TARGET_TYPE     VARCHAR(20) NOT NULL COMMENT '类型: personal/enterprise',
+    TARGET_ID       VARCHAR(36) NOT NULL COMMENT '目标ID'
+);
+COMMENT ON TABLE notification_target IS '消息接收人关联表';
+CREATE INDEX idx_nt_target ON notification_target(TARGET_TYPE, TARGET_ID);
+CREATE INDEX idx_nt_notify ON notification_target(NOTIFICATION_ID);

二进制
doc/~$市人力资源和社会保障局智慧人社运营运维(2025-2027年)项目需求规格说明书-就业一湛通服务平台.docx


+ 326 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/controller/NotificationController.java

@@ -0,0 +1,326 @@
+package org.jeecg.modules.zjrs.notification.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.shiro.SecurityUtils;
+import org.jeecg.common.api.vo.Result;
+import org.jeecg.common.system.vo.LoginUser;
+import org.jeecg.modules.zjrs.enterprise.entity.EnterpriseInfo;
+import org.jeecg.modules.zjrs.enterprise.mapper.EnterpriseInfoMapper;
+import org.jeecg.modules.zjrs.enterprisestatus.entity.EnterpriseStatusLocal;
+import org.jeecg.modules.zjrs.enterprisestatus.service.IEnterpriseStatusService;
+import org.jeecg.modules.zjrs.notification.entity.NotificationRecord;
+import org.jeecg.modules.zjrs.notification.entity.NotificationTarget;
+import org.jeecg.modules.zjrs.notification.mapper.NotificationTargetMapper;
+import org.jeecg.modules.zjrs.notification.service.INotificationRecordService;
+import org.jeecg.modules.zjrs.personal.entity.PersonalInfo;
+import org.jeecg.modules.zjrs.personal.mapper.PersonalInfoMapper;
+import org.jeecg.modules.zjrs.personalstatus.entity.PersonalStatusLocal;
+import org.jeecg.modules.zjrs.personalstatus.service.IPersonalStatusService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Tag(name = "消息通知")
+@RestController
+@RequestMapping("/notification")
+public class NotificationController {
+
+    @Autowired
+    private INotificationRecordService notificationRecordService;
+
+    @Autowired
+    private NotificationTargetMapper notificationTargetMapper;
+
+    @Autowired
+    private IEnterpriseStatusService enterpriseStatusService;
+
+    @Autowired
+    private IPersonalStatusService personalStatusService;
+
+    @Autowired
+    private PersonalInfoMapper personalInfoMapper;
+
+    @Autowired
+    private EnterpriseInfoMapper enterpriseInfoMapper;
+
+    @Operation(summary = "发送消息(统一接口,支持单条和批量)")
+    @PostMapping(value = "/send")
+    public Result<NotificationRecord> send(@RequestBody Map<String, Object> body) {
+        LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
+        String moduleType = (String) body.get("moduleType");
+        String subject = (String) body.get("subject");
+        String content = (String) body.get("content");
+        @SuppressWarnings("unchecked")
+        List<Map<String, String>> targets = (List<Map<String, String>>) body.get("targets");
+
+        if (targets == null || targets.isEmpty()) {
+            return Result.error("接收人不能为空");
+        }
+
+        Date now = new Date();
+        NotificationRecord record = new NotificationRecord();
+        record.setModuleType(moduleType);
+        record.setSubject(subject);
+        record.setContent(content);
+        record.setSenderId(sysUser.getId());
+        record.setSender(sysUser.getRealname());
+        record.setSendTime(now);
+        notificationRecordService.save(record);
+
+        for (Map<String, String> target : targets) {
+            String targetType = target.get("targetType");
+            String targetId = target.get("targetId");
+
+            NotificationTarget nt = new NotificationTarget();
+            nt.setNotificationId(record.getId());
+            nt.setTargetType(targetType);
+            nt.setTargetId(targetId);
+            notificationTargetMapper.insert(nt);
+
+            if ("enterprise".equals(targetType)) {
+                QueryWrapper<EnterpriseStatusLocal> q = new QueryWrapper<>();
+                q.eq("enterprise_id", targetId);
+                EnterpriseStatusLocal ext = enterpriseStatusService.getOne(q);
+                if (ext != null) {
+                    ext.setLastNoticeTime(now);
+                    enterpriseStatusService.updateById(ext);
+                } else {
+                    EnterpriseStatusLocal local = new EnterpriseStatusLocal();
+                    local.setEnterpriseId(targetId);
+                    local.setLastNoticeTime(now);
+                    enterpriseStatusService.save(local);
+                }
+            } else if ("personal".equals(targetType)) {
+                QueryWrapper<PersonalStatusLocal> q = new QueryWrapper<>();
+                q.eq("personal_id", targetId);
+                PersonalStatusLocal ext = personalStatusService.getOne(q);
+                if (ext != null) {
+                    ext.setLastNoticeTime(now);
+                    personalStatusService.updateById(ext);
+                } else {
+                    PersonalStatusLocal local = new PersonalStatusLocal();
+                    local.setPersonalId(targetId);
+                    local.setLastNoticeTime(now);
+                    personalStatusService.save(local);
+                }
+            }
+        }
+
+        return Result.OK("发送成功", record);
+    }
+
+    /** 批量关联查询目标名称 */
+    private void resolveTargetNames(List<NotificationTarget> targets) {
+        if (targets == null || targets.isEmpty()) return;
+        List<String> personalIds = targets.stream().filter(t -> "personal".equals(t.getTargetType())).map(NotificationTarget::getTargetId).collect(Collectors.toList());
+        List<String> enterpriseIds = targets.stream().filter(t -> "enterprise".equals(t.getTargetType())).map(NotificationTarget::getTargetId).collect(Collectors.toList());
+
+        Map<String, String> nameMap = new HashMap<>();
+        if (!personalIds.isEmpty()) {
+            QueryWrapper<PersonalInfo> pq = new QueryWrapper<>();
+            pq.in("id", personalIds);
+            pq.select("id", "full_name");
+            for (PersonalInfo p : personalInfoMapper.selectList(pq)) {
+                nameMap.put(p.getId(), p.getFullName());
+            }
+        }
+        if (!enterpriseIds.isEmpty()) {
+            QueryWrapper<EnterpriseInfo> eq = new QueryWrapper<>();
+            eq.in("id", enterpriseIds);
+            eq.select("id", "company_name");
+            for (EnterpriseInfo e : enterpriseInfoMapper.selectList(eq)) {
+                nameMap.put(e.getId(), e.getCompanyName());
+            }
+        }
+
+        for (NotificationTarget t : targets) {
+            t.setTargetName(nameMap.getOrDefault(t.getTargetId(), t.getTargetId()));
+        }
+    }
+
+    @Operation(summary = "查询当前用户发送的消息")
+    @GetMapping(value = "/myList")
+    public Result<Page<NotificationRecord>> myList(
+            @RequestParam(name = "keyword", required = false) String keyword,
+            @RequestParam(name = "moduleType", required = false) String moduleType,
+            @RequestParam(name = "targetType", required = false) String targetType,
+            @RequestParam(name = "targetName", required = false) String targetName,
+            @RequestParam(name = "beginDate", required = false) String beginDate,
+            @RequestParam(name = "endDate", required = false) String endDate,
+            @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
+            @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
+        LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
+        QueryWrapper<NotificationRecord> query = new QueryWrapper<>();
+        query.eq("sender", sysUser.getRealname());
+        if (keyword != null && !keyword.isEmpty()) {
+            query.like("subject", keyword);
+        }
+        if (moduleType != null && !moduleType.isEmpty()) {
+            query.eq("module_type", moduleType);
+        }
+        if (beginDate != null && !beginDate.isEmpty()) {
+            query.ge("send_time", beginDate + " 00:00:00");
+        }
+        if (endDate != null && !endDate.isEmpty()) {
+            query.le("send_time", endDate + " 23:59:59");
+        }
+
+        //update-begin---author:kk ---date:2026-06-11  for:【消息中心】已发送消息-新增接收人搜索条件-----------
+        // 根据接收人条件过滤消息记录
+        Set<String> filterNotificationIds = null;
+        boolean hasTargetFilter = (targetType != null && !targetType.isEmpty()) || (targetName != null && !targetName.isEmpty());
+        if (hasTargetFilter) {
+            QueryWrapper<NotificationTarget> tq = new QueryWrapper<>();
+            if (targetType != null && !targetType.isEmpty()) {
+                tq.eq("target_type", targetType);
+            }
+            // 按接收人姓名模糊搜索
+            if (targetName != null && !targetName.isEmpty()) {
+                Set<String> matchedTargetIds = new HashSet<>();
+                if (targetType == null || targetType.isEmpty() || "personal".equals(targetType)) {
+                    QueryWrapper<PersonalInfo> pq = new QueryWrapper<>();
+                    pq.like("full_name", targetName);
+                    pq.select("id");
+                    for (PersonalInfo p : personalInfoMapper.selectList(pq)) {
+                        matchedTargetIds.add(p.getId());
+                    }
+                }
+                if (targetType == null || targetType.isEmpty() || "enterprise".equals(targetType)) {
+                    QueryWrapper<EnterpriseInfo> eq = new QueryWrapper<>();
+                    eq.like("company_name", targetName);
+                    eq.select("id");
+                    for (EnterpriseInfo e : enterpriseInfoMapper.selectList(eq)) {
+                        matchedTargetIds.add(e.getId());
+                    }
+                }
+                if (matchedTargetIds.isEmpty()) {
+                    // 没有匹配的接收人,直接返回空结果
+                    Page<NotificationRecord> emptyPage = new Page<>(pageNo, pageSize);
+                    emptyPage.setTotal(0);
+                    emptyPage.setRecords(Collections.emptyList());
+                    return Result.OK(emptyPage);
+                }
+                tq.in("target_id", matchedTargetIds);
+            }
+            tq.select("notification_id");
+            filterNotificationIds = new HashSet<>();
+            for (NotificationTarget t : notificationTargetMapper.selectList(tq)) {
+                filterNotificationIds.add(t.getNotificationId());
+            }
+            if (filterNotificationIds.isEmpty()) {
+                Page<NotificationRecord> emptyPage = new Page<>(pageNo, pageSize);
+                emptyPage.setTotal(0);
+                emptyPage.setRecords(Collections.emptyList());
+                return Result.OK(emptyPage);
+            }
+            query.in("id", filterNotificationIds);
+        }
+        //update-end---author:kk ---date:2026-06-11  for:【消息中心】已发送消息-新增接收人搜索条件-----------
+
+        query.orderByDesc("send_time");
+        Page<NotificationRecord> page = new Page<>(pageNo, pageSize);
+        page = notificationRecordService.page(page, query);
+        for (NotificationRecord record : page.getRecords()) {
+            QueryWrapper<NotificationTarget> tq = new QueryWrapper<>();
+            tq.eq("notification_id", record.getId());
+            List<NotificationTarget> targets = notificationTargetMapper.selectList(tq);
+            resolveTargetNames(targets);
+            record.setTargets(targets);
+        }
+        return Result.OK(page);
+    }
+
+    @Operation(summary = "分页查询消息记录(所有人)")
+    @GetMapping(value = "/list")
+    public Result<Page<NotificationRecord>> list(
+            @RequestParam(name = "keyword", required = false) String keyword,
+            @RequestParam(name = "moduleType", required = false) String moduleType,
+            @RequestParam(name = "targetType", required = false) String targetType,
+            @RequestParam(name = "targetName", required = false) String targetName,
+            @RequestParam(name = "beginDate", required = false) String beginDate,
+            @RequestParam(name = "endDate", required = false) String endDate,
+            @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
+            @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize) {
+        QueryWrapper<NotificationRecord> query = new QueryWrapper<>();
+        if (keyword != null && !keyword.isEmpty()) {
+            query.like("subject", keyword);
+        }
+        if (moduleType != null && !moduleType.isEmpty()) {
+            query.eq("module_type", moduleType);
+        }
+        if (beginDate != null && !beginDate.isEmpty()) {
+            query.ge("send_time", beginDate + " 00:00:00");
+        }
+        if (endDate != null && !endDate.isEmpty()) {
+            query.le("send_time", endDate + " 23:59:59");
+        }
+
+        //update-begin---author:kk ---date:2026-06-11  for:【消息中心】全部已发送消息-新增接收人搜索条件-----------
+        Set<String> filterNotificationIds = null;
+        boolean hasTargetFilter = (targetType != null && !targetType.isEmpty()) || (targetName != null && !targetName.isEmpty());
+        if (hasTargetFilter) {
+            QueryWrapper<NotificationTarget> tq = new QueryWrapper<>();
+            if (targetType != null && !targetType.isEmpty()) {
+                tq.eq("target_type", targetType);
+            }
+            if (targetName != null && !targetName.isEmpty()) {
+                Set<String> matchedTargetIds = new HashSet<>();
+                if (targetType == null || targetType.isEmpty() || "personal".equals(targetType)) {
+                    QueryWrapper<PersonalInfo> pq = new QueryWrapper<>();
+                    pq.like("full_name", targetName);
+                    pq.select("id");
+                    for (PersonalInfo p : personalInfoMapper.selectList(pq)) {
+                        matchedTargetIds.add(p.getId());
+                    }
+                }
+                if (targetType == null || targetType.isEmpty() || "enterprise".equals(targetType)) {
+                    QueryWrapper<EnterpriseInfo> eq = new QueryWrapper<>();
+                    eq.like("company_name", targetName);
+                    eq.select("id");
+                    for (EnterpriseInfo e : enterpriseInfoMapper.selectList(eq)) {
+                        matchedTargetIds.add(e.getId());
+                    }
+                }
+                if (matchedTargetIds.isEmpty()) {
+                    Page<NotificationRecord> emptyPage = new Page<>(pageNo, pageSize);
+                    emptyPage.setTotal(0);
+                    emptyPage.setRecords(Collections.emptyList());
+                    return Result.OK(emptyPage);
+                }
+                tq.in("target_id", matchedTargetIds);
+            }
+            tq.select("notification_id");
+            filterNotificationIds = new HashSet<>();
+            for (NotificationTarget t : notificationTargetMapper.selectList(tq)) {
+                filterNotificationIds.add(t.getNotificationId());
+            }
+            if (filterNotificationIds.isEmpty()) {
+                Page<NotificationRecord> emptyPage = new Page<>(pageNo, pageSize);
+                emptyPage.setTotal(0);
+                emptyPage.setRecords(Collections.emptyList());
+                return Result.OK(emptyPage);
+            }
+            query.in("id", filterNotificationIds);
+        }
+        //update-end---author:kk ---date:2026-06-11  for:【消息中心】全部已发送消息-新增接收人搜索条件-----------
+
+        query.orderByDesc("send_time");
+        Page<NotificationRecord> page = new Page<>(pageNo, pageSize);
+        page = notificationRecordService.page(page, query);
+        for (NotificationRecord record : page.getRecords()) {
+            QueryWrapper<NotificationTarget> tq = new QueryWrapper<>();
+            tq.eq("notification_id", record.getId());
+            List<NotificationTarget> targets = notificationTargetMapper.selectList(tq);
+            resolveTargetNames(targets);
+            record.setTargets(targets);
+        }
+        return Result.OK(page);
+    }
+}

+ 63 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/entity/NotificationRecord.java

@@ -0,0 +1,63 @@
+package org.jeecg.modules.zjrs.notification.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+@Data
+@TableName("notification_record")
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+@Schema(description = "消息通知记录表")
+public class NotificationRecord implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.ASSIGN_ID)
+    @Schema(description = "主键ID")
+    private String id;
+
+    @Schema(description = "来源模块")
+    private String moduleType;
+
+    @Schema(description = "消息主题")
+    private String subject;
+
+    @Schema(description = "消息内容")
+    private String content;
+
+    @Schema(description = "推送人用户ID")
+    private String senderId;
+
+    @Schema(description = "推送人姓名")
+    private String sender;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Schema(description = "推送时间")
+    private Date sendTime;
+
+    @Schema(description = "创建人")
+    private String createBy;
+
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Schema(description = "创建时间")
+    private Date createTime;
+
+    @Schema(description = "组织机构编号")
+    private String sysOrgCode;
+
+    // 非表字段 — 接收人列表(用于查询时关联填充)
+    @Schema(description = "接收人列表")
+    private transient List<NotificationTarget> targets;
+}

+ 33 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/entity/NotificationTarget.java

@@ -0,0 +1,33 @@
+package org.jeecg.modules.zjrs.notification.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+@Data
+@TableName("notification_target")
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+@Schema(description = "消息接收人关联表")
+public class NotificationTarget implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "消息ID")
+    private String notificationId;
+
+    @Schema(description = "类型: personal/enterprise")
+    private String targetType;
+
+    @Schema(description = "目标ID")
+    private String targetId;
+
+    // 非表字段 — 查询时从 PERSONAL_INFO / ENTERPRISE_INFO 关联填充
+    @TableField(exist = false)
+    @Schema(description = "目标名称(关联查询)")
+    private String targetName;
+}

+ 7 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/mapper/NotificationRecordMapper.java

@@ -0,0 +1,7 @@
+package org.jeecg.modules.zjrs.notification.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.jeecg.modules.zjrs.notification.entity.NotificationRecord;
+
+public interface NotificationRecordMapper extends BaseMapper<NotificationRecord> {
+}

+ 7 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/mapper/NotificationTargetMapper.java

@@ -0,0 +1,7 @@
+package org.jeecg.modules.zjrs.notification.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.jeecg.modules.zjrs.notification.entity.NotificationTarget;
+
+public interface NotificationTargetMapper extends BaseMapper<NotificationTarget> {
+}

+ 4 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/mapper/xml/NotificationRecordMapper.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.jeecg.modules.zjrs.notification.mapper.NotificationRecordMapper">
+</mapper>

+ 7 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/service/INotificationRecordService.java

@@ -0,0 +1,7 @@
+package org.jeecg.modules.zjrs.notification.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.jeecg.modules.zjrs.notification.entity.NotificationRecord;
+
+public interface INotificationRecordService extends IService<NotificationRecord> {
+}

+ 11 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/notification/service/impl/NotificationRecordServiceImpl.java

@@ -0,0 +1,11 @@
+package org.jeecg.modules.zjrs.notification.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.jeecg.modules.zjrs.notification.entity.NotificationRecord;
+import org.jeecg.modules.zjrs.notification.mapper.NotificationRecordMapper;
+import org.jeecg.modules.zjrs.notification.service.INotificationRecordService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class NotificationRecordServiceImpl extends ServiceImpl<NotificationRecordMapper, NotificationRecord> implements INotificationRecordService {
+}

+ 23 - 0
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260611_1__create_notification_record.sql

@@ -0,0 +1,23 @@
+-- 消息通知记录表 + 接收人关联表(MySQL)
+-- 设计:消息主题/内容只存一份,接收人独立关联,批量发送无冗余
+
+CREATE TABLE IF NOT EXISTS `notification_record` (
+    `id`            VARCHAR(36)  NOT NULL COMMENT '主键ID',
+    `module_type`   VARCHAR(50)           COMMENT '来源模块',
+    `subject`       VARCHAR(200) NOT NULL COMMENT '消息主题',
+    `content`       VARCHAR(2000)         COMMENT '消息内容',
+    `sender`        VARCHAR(50)  NOT NULL COMMENT '推送人',
+    `send_time`     DATETIME     NOT NULL COMMENT '推送时间',
+    `create_by`     VARCHAR(50)           COMMENT '创建人',
+    `create_time`   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `sys_org_code`  VARCHAR(50)           COMMENT '组织机构编号',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息通知记录表';
+
+CREATE TABLE IF NOT EXISTS `notification_target` (
+    `notification_id` VARCHAR(36) NOT NULL COMMENT '消息ID',
+    `target_type`     VARCHAR(20) NOT NULL COMMENT '类型: personal/enterprise',
+    `target_id`       VARCHAR(36) NOT NULL COMMENT '目标ID',
+    INDEX `idx_nt_target` (`target_type`, `target_id`),
+    INDEX `idx_nt_notify` (`notification_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息接收人关联表';

+ 12 - 0
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260611_2__menu_insert_SentMessages.sql

@@ -0,0 +1,12 @@
+-- ============================================================
+-- 就业一湛通服务平台 - 已发送消息 菜单权限SQL
+-- 挂载在 就业状态自动感知(178060400000000) 下
+-- ============================================================
+
+-- 1. 二级菜单:已发送消息
+INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
+VALUES ('178060400000003', '178060400000000', '已发送消息', '/employmentStatus/sentMessages', 'notification/SentMessagesList', 1, 'SentMessagesList', NULL, 1, 'notification:list', '1', 3.00, 0, 'ant-design:send-outlined', 1, 1, 0, 0, NULL, 'admin', NOW(), NULL, NULL, 0, 0, 1, 0);
+
+-- 2. 授权:给admin角色分配菜单权限
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip)
+VALUES ('178060400000003_admin', 'f6817f48af4fb3af11b9e8bf182f618b', '178060400000003', NULL, NOW(), '127.0.0.1');

+ 12 - 0
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260611_3__menu_insert_AllMessages.sql

@@ -0,0 +1,12 @@
+-- ============================================================
+-- 就业一湛通服务平台 - 全部已发送消息 菜单权限SQL
+-- 挂载在消息中心(178060400000004)下,与已发送消息(178060400000003)同级
+-- ============================================================
+
+-- 1. 二级菜单:全部已发送消息(sort_no=2.00 排在已发送消息之后)
+INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external)
+VALUES ('178060400000005', '178060400000004', '全部已发送消息', '/messageCenter/allMessages', 'notification/AllMessagesList', 1, 'AllMessagesList', NULL, 1, 'notification:allList', '1', 2.00, 0, 'ant-design:message-outlined', 1, 1, 0, 0, NULL, 'admin', NOW(), NULL, NULL, 0, 0, 1, 0);
+
+-- 2. 授权:给admin角色分配菜单权限
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip)
+VALUES ('178060400000005_admin', 'f6817f48af4fb3af11b9e8bf182f618b', '178060400000005', NULL, NOW(), '127.0.0.1');

+ 5 - 0
jeecgboot-vue3/src/views/notification/AllMessages.api.ts

@@ -0,0 +1,5 @@
+import { defHttp } from '/@/utils/http/axios';
+
+export function allList(params) {
+  return defHttp.get({ url: '/notification/list', params });
+}

+ 10 - 0
jeecgboot-vue3/src/views/notification/AllMessages.data.ts

@@ -0,0 +1,10 @@
+import { BasicColumn } from '/@/components/Table';
+
+export const columns: BasicColumn[] = [
+  { title: '消息主题', dataIndex: 'subject', width: 180 },
+  { title: '消息内容', dataIndex: 'content', width: 200, ellipsis: true },
+  { title: '接收人数', dataIndex: 'targets', width: 90 },
+  { title: '来源模块', dataIndex: 'moduleType', width: 150 },
+  { title: '推送人', dataIndex: 'sender', width: 100 },
+  { title: '推送时间', dataIndex: 'sendTime', width: 170 },
+];

+ 175 - 0
jeecgboot-vue3/src/views/notification/AllMessagesList.vue

@@ -0,0 +1,175 @@
+<template>
+  <div class="p-2">
+    <div class="jeecg-basic-table-form-container">
+      <a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
+        <a-row :gutter="24">
+          <a-col :lg="6" :md="8" :sm="24">
+            <a-form-item label="关键字" name="keyword">
+              <a-input v-model:value="queryParam.keyword" placeholder="搜索消息主题" allow-clear />
+            </a-form-item>
+          </a-col>
+          <a-col :lg="6" :md="8" :sm="24">
+            <a-form-item label="来源模块" name="moduleType">
+              <a-select v-model:value="queryParam.moduleType" :options="moduleOptions" placeholder="全部模块" allow-clear />
+            </a-form-item>
+          </a-col>
+          <a-col :lg="6" :md="8" :sm="24">
+            <a-form-item label="推送时间" name="dateRange">
+              <a-range-picker v-model:value="dateRange" :placeholder="['开始日期', '结束日期']" format="YYYY-MM-DD" value-format="YYYY-MM-DD" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :lg="4" :md="4" :sm="24">
+            <a-form-item label="接收人类型" name="targetType">
+              <a-select v-model:value="queryParam.targetType" :options="targetTypeOptions" placeholder="全部" allow-clear />
+            </a-form-item>
+          </a-col>
+          <a-col :lg="4" :md="4" :sm="24">
+            <a-form-item label="接收人" name="targetName">
+              <a-input v-model:value="queryParam.targetName" placeholder="接收人姓名" allow-clear />
+            </a-form-item>
+          </a-col>
+          <a-col :lg="4" :md="4" :sm="24">
+            <a-form-item>
+              <a-space>
+                <a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
+                <a-button preIcon="ant-design:reload-outlined" @click="searchReset">重置</a-button>
+              </a-space>
+            </a-form-item>
+          </a-col>
+        </a-row>
+      </a-form>
+    </div>
+
+    <BasicTable @register="registerTable">
+      <template #bodyCell="{ column, text, record }">
+        <template v-if="column.dataIndex === 'content'">
+          <span class="content-preview">{{ stripHtml(text) }}</span>
+        </template>
+        <template v-else-if="column.dataIndex === 'targets'">
+          <span>{{ (text && text.length) || 0 }}人</span>
+        </template>
+        <template v-else-if="column.dataIndex === 'moduleType'">
+          <a-tag>{{ moduleTypeMap[text] || text }}</a-tag>
+        </template>
+      </template>
+      <template #action="{ record }">
+        <a-button type="link" size="small" @click="openDetail(record)">详情</a-button>
+      </template>
+    </BasicTable>
+
+    <a-modal v-model:visible="detailVisible" title="消息详情" :footer="null" width="700px" @cancel="detailVisible = false">
+      <a-descriptions bordered :column="1" size="small">
+        <a-descriptions-item label="消息主题">{{ detailRecord?.subject }}</a-descriptions-item>
+        <a-descriptions-item label="接收人">
+          <template v-if="detailRecord?.targets?.length">
+            <a-tag v-for="t in detailRecord.targets" :key="t.targetId" style="margin: 2px 4px 2px 0">{{ t.targetName || t.targetId }}</a-tag>
+          </template>
+          <span v-else>—</span>
+        </a-descriptions-item>
+        <a-descriptions-item label="来源模块">
+          <a-tag>{{ moduleTypeMap[detailRecord?.moduleType] || detailRecord?.moduleType }}</a-tag>
+        </a-descriptions-item>
+        <a-descriptions-item label="推送人">{{ detailRecord?.sender }}</a-descriptions-item>
+        <a-descriptions-item label="推送时间">{{ detailRecord?.sendTime }}</a-descriptions-item>
+        <a-descriptions-item label="消息内容">
+          <div class="detail-content" v-html="detailRecord?.content" />
+        </a-descriptions-item>
+      </a-descriptions>
+    </a-modal>
+  </div>
+</template>
+
+<script lang="ts" name="allMessages" setup>
+  import { reactive, ref, computed } from 'vue';
+  import { BasicTable } from '/@/components/Table';
+  import { useListPage } from '/@/hooks/system/useListPage';
+  import { useDict } from '/@/hooks/dictionary/useDict';
+  import { columns } from './AllMessages.data';
+  import { allList } from './AllMessages.api';
+
+  const formRef = ref();
+  const queryParam = reactive<any>({});
+  const dateRange = ref<any[]>([]);
+  const detailVisible = ref(false);
+  const detailRecord = ref<any>(null);
+
+  const { dictMap, getDictText, getDictOptions } = useDict(['notification_module']);
+
+  const moduleTypeMap = computed(() => {
+    const map: Record<string, string> = {};
+    for (const item of (dictMap['notification_module'] || [])) {
+      if ((item as any).code) map[(item as any).code] = item.label;
+    }
+    return map;
+  });
+
+  const moduleOptions = computed(() =>
+    getDictOptions('notification_module').map((item) => ({
+      value: (item as any).code || item.value,
+      label: item.label,
+    }))
+  );
+
+  const targetTypeOptions = [
+    { value: 'personal', label: '个人' },
+    { value: 'enterprise', label: '企业' },
+  ];
+
+  function stripHtml(html: string) {
+    if (!html) return '';
+    return html.replace(/<[^>]+>/g, '').substring(0, 50) + (html.length > 50 ? '...' : '');
+  }
+
+  function openDetail(record: any) {
+    detailRecord.value = record;
+    detailVisible.value = true;
+  }
+
+  const { tableContext } = useListPage({
+    tableProps: {
+      title: '全部已发送消息',
+      api: allList,
+      columns,
+      canResize: true,
+      useSearchForm: false,
+      actionColumn: { width: 100, fixed: 'right' },
+      beforeFetch: (params) => {
+        Object.assign(params, queryParam);
+        if (dateRange.value && dateRange.value.length === 2) {
+          params.beginDate = dateRange.value[0];
+          params.endDate = dateRange.value[1];
+        }
+        return params;
+      },
+    },
+  });
+  const [registerTable, { reload }] = tableContext;
+  const labelCol = reactive({ xs: 24, sm: 6, md: 8, xl: 6, xxl: 6 });
+  const wrapperCol = reactive({ xs: 24, sm: 18 });
+
+  function searchQuery() { reload(); }
+  function searchReset() {
+    formRef.value.resetFields();
+    dateRange.value = [];
+    reload();
+  }
+</script>
+
+<style lang="less" scoped>
+  .jeecg-basic-table-form-container {
+    padding: 0;
+    .ant-form-item:not(.ant-form-item-with-help) { margin-bottom: 16px; height: 32px; }
+  }
+  .content-preview {
+    cursor: pointer;
+    color: #1890ff;
+    &:hover { text-decoration: underline; }
+  }
+  .detail-content {
+    max-height: 400px;
+    overflow-y: auto;
+    padding: 12px;
+    background: #fafafa;
+    border-radius: 4px;
+  }
+</style>

+ 5 - 0
jeecgboot-vue3/src/views/notification/SentMessages.api.ts

@@ -0,0 +1,5 @@
+import { defHttp } from '/@/utils/http/axios';
+
+export function myList(params) {
+  return defHttp.get({ url: '/notification/myList', params });
+}

+ 9 - 0
jeecgboot-vue3/src/views/notification/SentMessages.data.ts

@@ -0,0 +1,9 @@
+import { BasicColumn } from '/@/components/Table';
+
+export const columns: BasicColumn[] = [
+  { title: '消息主题', dataIndex: 'subject', width: 200 },
+  { title: '消息内容', dataIndex: 'content', width: 200, ellipsis: true },
+  { title: '接收人数', dataIndex: 'targets', width: 90 },
+  { title: '来源模块', dataIndex: 'moduleType', width: 150 },
+  { title: '推送时间', dataIndex: 'sendTime', width: 170 },
+];

+ 175 - 0
jeecgboot-vue3/src/views/notification/SentMessagesList.vue

@@ -0,0 +1,175 @@
+<template>
+  <div class="p-2">
+    <div class="jeecg-basic-table-form-container">
+      <a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
+        <a-row :gutter="24">
+          <a-col :lg="6" :md="8" :sm="24">
+            <a-form-item label="关键字" name="keyword">
+              <a-input v-model:value="queryParam.keyword" placeholder="搜索消息主题" allow-clear />
+            </a-form-item>
+          </a-col>
+          <a-col :lg="6" :md="8" :sm="24">
+            <a-form-item label="来源模块" name="moduleType">
+              <a-select v-model:value="queryParam.moduleType" :options="moduleOptions" placeholder="全部模块" allow-clear />
+            </a-form-item>
+          </a-col>
+          <a-col :lg="6" :md="8" :sm="24">
+            <a-form-item label="推送时间" name="dateRange">
+              <a-range-picker v-model:value="dateRange" :placeholder="['开始日期', '结束日期']" format="YYYY-MM-DD" value-format="YYYY-MM-DD" style="width: 100%" />
+            </a-form-item>
+          </a-col>
+          <a-col :lg="4" :md="4" :sm="24">
+            <a-form-item label="接收人类型" name="targetType">
+              <a-select v-model:value="queryParam.targetType" :options="targetTypeOptions" placeholder="全部" allow-clear />
+            </a-form-item>
+          </a-col>
+          <a-col :lg="4" :md="4" :sm="24">
+            <a-form-item label="接收人" name="targetName">
+              <a-input v-model:value="queryParam.targetName" placeholder="接收人姓名" allow-clear />
+            </a-form-item>
+          </a-col>
+          <a-col :lg="4" :md="4" :sm="24">
+            <a-form-item>
+              <a-space>
+                <a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
+                <a-button preIcon="ant-design:reload-outlined" @click="searchReset">重置</a-button>
+              </a-space>
+            </a-form-item>
+          </a-col>
+        </a-row>
+      </a-form>
+    </div>
+
+    <BasicTable @register="registerTable">
+      <template #bodyCell="{ column, text, record }">
+        <template v-if="column.dataIndex === 'content'">
+          <span class="content-preview">{{ stripHtml(text) }}</span>
+        </template>
+        <template v-else-if="column.dataIndex === 'targets'">
+          <span>{{ (text && text.length) || 0 }}人</span>
+        </template>
+        <template v-else-if="column.dataIndex === 'moduleType'">
+          <a-tag>{{ moduleTypeMap[text] || text }}</a-tag>
+        </template>
+      </template>
+      <template #action="{ record }">
+        <a-button type="link" size="small" @click="openDetail(record)">详情</a-button>
+      </template>
+    </BasicTable>
+
+    <a-modal v-model:visible="detailVisible" title="消息详情" :footer="null" width="700px" @cancel="detailVisible = false">
+      <a-descriptions bordered :column="1" size="small">
+        <a-descriptions-item label="消息主题">{{ detailRecord?.subject }}</a-descriptions-item>
+        <a-descriptions-item label="接收人">
+          <template v-if="detailRecord?.targets?.length">
+            <a-tag v-for="t in detailRecord.targets" :key="t.targetId" style="margin: 2px 4px 2px 0">{{ t.targetName || t.targetId }}</a-tag>
+          </template>
+          <span v-else>—</span>
+        </a-descriptions-item>
+        <a-descriptions-item label="来源模块">
+          <a-tag>{{ moduleTypeMap[detailRecord?.moduleType] || detailRecord?.moduleType }}</a-tag>
+        </a-descriptions-item>
+        <a-descriptions-item label="推送人">{{ detailRecord?.sender }}</a-descriptions-item>
+        <a-descriptions-item label="推送时间">{{ detailRecord?.sendTime }}</a-descriptions-item>
+        <a-descriptions-item label="消息内容">
+          <div class="detail-content" v-html="detailRecord?.content" />
+        </a-descriptions-item>
+      </a-descriptions>
+    </a-modal>
+  </div>
+</template>
+
+<script lang="ts" name="sentMessages" setup>
+  import { reactive, ref, computed } from 'vue';
+  import { BasicTable } from '/@/components/Table';
+  import { useListPage } from '/@/hooks/system/useListPage';
+  import { useDict } from '/@/hooks/dictionary/useDict';
+  import { columns } from './SentMessages.data';
+  import { myList } from './SentMessages.api';
+
+  const formRef = ref();
+  const queryParam = reactive<any>({});
+  const dateRange = ref<any[]>([]);
+  const detailVisible = ref(false);
+  const detailRecord = ref<any>(null);
+
+  const { dictMap, getDictText, getDictOptions } = useDict(['notification_module']);
+
+  const moduleTypeMap = computed(() => {
+    const map: Record<string, string> = {};
+    for (const item of (dictMap['notification_module'] || [])) {
+      if ((item as any).code) map[(item as any).code] = item.label;
+    }
+    return map;
+  });
+
+  const moduleOptions = computed(() =>
+    getDictOptions('notification_module').map((item) => ({
+      value: (item as any).code || item.value,
+      label: item.label,
+    }))
+  );
+
+  const targetTypeOptions = [
+    { value: 'personal', label: '个人' },
+    { value: 'enterprise', label: '企业' },
+  ];
+
+  function stripHtml(html: string) {
+    if (!html) return '';
+    return html.replace(/<[^>]+>/g, '').substring(0, 50) + (html.length > 50 ? '...' : '');
+  }
+
+  function openDetail(record: any) {
+    detailRecord.value = record;
+    detailVisible.value = true;
+  }
+
+  const { tableContext } = useListPage({
+    tableProps: {
+      title: '已发送消息',
+      api: myList,
+      columns,
+      canResize: true,
+      useSearchForm: false,
+      actionColumn: { width: 100, fixed: 'right' },
+      beforeFetch: (params) => {
+        Object.assign(params, queryParam);
+        if (dateRange.value && dateRange.value.length === 2) {
+          params.beginDate = dateRange.value[0];
+          params.endDate = dateRange.value[1];
+        }
+        return params;
+      },
+    },
+  });
+  const [registerTable, { reload }] = tableContext;
+  const labelCol = reactive({ xs: 24, sm: 6, md: 8, xl: 6, xxl: 6 });
+  const wrapperCol = reactive({ xs: 24, sm: 18 });
+
+  function searchQuery() { reload(); }
+  function searchReset() {
+    formRef.value.resetFields();
+    dateRange.value = [];
+    reload();
+  }
+</script>
+
+<style lang="less" scoped>
+  .jeecg-basic-table-form-container {
+    padding: 0;
+    .ant-form-item:not(.ant-form-item-with-help) { margin-bottom: 16px; height: 32px; }
+  }
+  .content-preview {
+    cursor: pointer;
+    color: #1890ff;
+    &:hover { text-decoration: underline; }
+  }
+  .detail-content {
+    max-height: 400px;
+    overflow-y: auto;
+    padding: 12px;
+    background: #fafafa;
+    border-radius: 4px;
+  }
+</style>