别小看"系统通知公告"!企业管理系统中最被低估的功能,居然是软件设计中一个经典案例

0 阅读21分钟

别小看"系统通知公告"!企业管理系统中最被低估的功能,居然是软件设计中一个经典案例

🌐 演示地址ruoyioffice.com | 📦 源码1gitee.com/yqzy1688/ru… |📦 源码2gitee.com/yqzy1688/ru… |📦 源码3github.com/yuqing2026/… | 💬:17156169080(备注「RuoYi Office」)

在企业管理系统中,有这样一个功能——几乎每个系统都有,却很少有人认真设计。它就是通知公告。做好了,它是企业信息触达的高速公路;做不好,它就是一个没人看的摆设。RuoYi Office 用一套完整的架构,把这个「小功能」做出了「大格局」。

引言:通知公告,真的只是"发个通知"吗?

作为开发者,你可能觉得通知公告是系统中最简单的模块——无非就是一个 CRUD 嘛。标题、内容、类型,存数据库,列表展示,完事。

但如果你曾经在企业中工作过,你就会知道现实远比想象复杂:

  • 老板发了一个紧急通知,3 天后才发现还有人没看——没有已读追踪
  • 新员工入职,不知道公司有哪些规章制度——历史公告无法回溯
  • IT 部门发了系统维护通知,结果业务部门照常操作导致数据丢失——通知触达不了真正需要的人
  • 管理层发了机密通知,结果全员可见——缺乏权限控制
  • 重要通知混在一堆日常通知里,没人注意到——缺乏优先级标识

这些痛点,折射出的是一个核心问题:通知公告不是简单的信息发布,而是企业级的信息触达体系

维度简单 CRUD 方案企业级方案(RuoYi Office)
发布管理员写内容、保存富文本编辑 + 类型分类 + 重要标识
触达用户自己去列表看WebSocket 实时推送 + 首页组件提醒
追踪已读/未读追踪 + 阅读时间记录
权限全员可见RBAC 权限控制 + 多租户隔离
交互纯列表首页集成 + 弹窗预览 + 徽章提醒

接下来,我们将完整拆解 RuoYi Office 是如何将通知公告做成一个企业级信息触达体系的。

功能全景:RuoYi Office 通知公告长什么样?

在深入技术细节之前,先来看看 RuoYi Office 通知公告的实际效果。

1. 通知公告管理页面

管理员可以在后台管理所有通知公告,支持搜索、筛选、新增、编辑、推送、批量删除等操作。 notice-list-page.png

▲ 通知公告管理列表:支持按公告标题、类型、状态、是否重要通知等条件筛选,一目了然

从截图中可以看到几个亮点:

  • 多维度搜索:支持按公告标题、公告类型(通知公告/公司动态/行业咨询/规章制度)、公告状态、是否重要通知进行筛选
  • 已读状态追踪:每条公告都有已读/未读状态标记,管理员可以清楚知道信息触达情况
  • 批量操作:支持勾选多条公告进行批量删除,提升管理效率
  • 一键推送:每条公告都支持单独推送,通过 WebSocket 实时触达在线用户

2. 公告编辑表单

新增或编辑公告时,系统提供了丰富的编辑能力。 notice-create-form.png

▲ 公告编辑表单:集成 TinyMCE 富文本编辑器,支持图片上传、表格插入等丰富排版能力

表单的设计体现了几个核心理念:

  • 富文本编辑:集成 TinyMCE 编辑器,支持图片上传、表格、格式化等功能,公告内容不再是枯燥的纯文本
  • 类型分类:通过字典管理公告类型,可灵活扩展(通知公告、公司动态、行业咨询、规章制度等)
  • 重要标识:一键标记"重要通知",前端会以醒目的红色标签展示
  • 状态控制:开启/关闭状态,关闭的公告不再对普通用户展示

3. 首页通知公告组件

工作台首页右侧集成了通知公告组件,员工登录后第一时间就能看到最新通知。

notice-workbench-home.png

▲ 工作台首页:右上角通知公告组件,带未读小圆点、重要标签、查看更多入口,信息一目了然

首页组件的设计亮点:

  • 未读小圆点:橙色圆点标识未读通知,灰色表示已读,视觉直观
  • 重要标签:重要通知有醒目的红色「重要」标签
  • 类型前缀:每条通知前显示【通知公告】【公司动态】等类型标识
  • 点击预览:点击任意通知弹出详情预览弹窗,自动标记为已读
  • 查看更多:一键跳转到完整的通知公告列表

4. 公告查看:从"未读"到"已读"的完整链路

通知公告不只是「发布出去就完事了」,更关键的是确保信息真正触达每一位员工。RuoYi Office 围绕公告查看设计了一条完整的交互链路:首页感知 → 点击预览 → 自动已读 → 查看更多

已读状态实时变化

下图展示了首页通知公告组件的完整交互:未读通知以橙色圆点醒目标识,徽章数字提示未读总数;用户点击任意一条通知后,弹出详情预览弹窗——弹窗打开的瞬间,系统自动在后台调用 markAsRead 接口标记已读,无需用户手动操作。

notice-workbench-home.png

▲ 首页通知公告组件:橙色圆点 = 未读,灰色圆点 = 已读,徽章数字实时反映未读总数

当用户点击第一条未读通知"2026年企业管理数字化转型趋势分析"后,弹出详情预览弹窗: notice-preview-modal.png

▲ 公告详情预览弹窗:展示类型标签、重要标识、发布人、发布时间和富文本内容

关闭弹窗后,列表自动刷新——原本橙色的未读圆点变为灰色,徽章数字同步减少:

notice-after-read.png

▲ 已读后状态变化:第一条通知的圆点从橙色变为灰色,整个过程无感知、零操作

这个「阅读即已读」的设计,相比要求用户手动点击"标记已读"按钮的方案,体验更加自然流畅。背后的实现逻辑是前端 watch 监听弹窗的 visible 状态:

watch(
  () => props.visible,
  (visible) => {
    if (visible && props.notice?.id && props.notice.readStatus !== 1) {
      markNoticeAsRead(props.notice.id)       // 后台静默标记
        .then(() => emit('refresh'))           // 刷新列表更新状态
        .catch(console.error);
    }
  },
);
"查看更多":普通用户的公告全览页

点击首页组件右上角的**「查看更多」**链接,系统会跳转到一个独立的公告全览页面(路由 /notice/view)。这个页面与管理后台的公告管理页有本质区别:

对比维度管理页面(/system/messages/notice查看页面(/notice/view
面向对象系统管理员全体员工
权限要求需要 system:notice:create/update/delete仅需登录,无额外权限
功能范围增删改查 + 推送 + 批量操作仅查看 + 搜索筛选
操作按钮编辑、推送、删除仅"查看详情"
菜单可见显示在系统管理菜单下隐藏菜单(hideInMenu: true),通过首页组件入口进入

查看页面的路由配置特意设置了 hideInMenu: true,意味着这个页面不会出现在左侧导航菜单中——它只能通过首页通知组件的「查看更多」链接进入。这种设计避免了普通员工在菜单中看到一个「没有权限操作」的页面,保持了界面的干净整洁。

查看页面的操作也做了精简,每条公告只保留一个「查看详情」按钮,点击后同样弹出预览弹窗并自动标记已读:

// 查看页面的操作列 —— 仅保留"查看详情"
<TableAction
  :actions="[
    {
      label: '查看详情',
      type: 'link',
      icon: ACTION_ICON.VIEW,
      onClick: handleView.bind(null, row),
    },
  ]"
/>
权限控制的精妙之处

通知公告的权限设计体现了**「管理权与阅读权分离」**的原则:

  1. 阅读不设权限门槛markAsRead 接口不加 @PreAuthorize 注解,任何登录用户都能标记自己的已读状态。这是合理的——你不能阻止员工阅读发给他的通知
  2. 查看页面无管理权限/notice/view 路由不配置额外权限,登录即可访问。前端调用的 getNoticePage 接口使用的是通用查询权限
  3. 管理操作严格管控:创建、编辑、删除、推送等操作都需要对应的 system:notice:* 权限
  4. 多租户自动隔离:即使所有用户都能查看公告,MyBatis Plus 的租户拦截器也会自动注入 tenant_id 条件,确保 A 公司的员工看不到 B 公司的公告

数据库设计:两张表撑起整个体系

好的数据库设计是成功的一半。RuoYi Office 的通知公告用 2 张核心数据表 实现了完整的功能体系——简洁而不简单。

ER 关系总览

┌───────────────────────────────┐
│       system_notice           │
│       (通知公告表)             │
│  ├ id (公告ID)                │
│  ├ title (标题)               │
│  ├ content (内容)             │
│  ├ type (公告类型)             │
│  ├ status (状态)              │
│  ├ is_important (是否重要)     │
│  ├ creator (创建者)            │
│  ├ create_time (创建时间)      │
│  ├ tenant_id (租户编号)        │
│  └ deleted (逻辑删除)          │
└───────────────┬───────────────┘
                │ 1 : N
                ▼
┌───────────────────────────────┐
│     system_notice_read        │
│     (公告已读关系表)           │
│  ├ id (主键)                  │
│  ├ notice_id (公告ID)  ──── FK│
│  ├ user_id (用户ID)           │
│  ├ read_time (阅读时间)        │
│  └ tenant_id (租户编号)        │
└───────────────────────────────┘

表1:system_notice — 通知公告表

CREATE TABLE `system_notice` (
  `id`           bigint       NOT NULL AUTO_INCREMENT COMMENT '公告ID',
  `title`        varchar(50)  NOT NULL COMMENT '公告标题',
  `content`      text         NOT NULL COMMENT '公告内容',
  `type`         tinyint      NOT NULL COMMENT '公告类型(1通知 2公告)',
  `status`       tinyint      NOT NULL DEFAULT 0 COMMENT '公告状态(0正常 1关闭)',
  `is_important` bit(1)       NOT NULL DEFAULT b'0' COMMENT '是否重要通知(0否 1是)',
  `creator`      varchar(64)  NULL DEFAULT '' COMMENT '创建者',
  `create_time`  datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater`      varchar(64)  NULL DEFAULT '' COMMENT '更新者',
  `update_time`  datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `deleted`      bit(1)       NOT NULL DEFAULT b'0' COMMENT '是否删除',
  `tenant_id`    bigint       NOT NULL DEFAULT 0 COMMENT '租户编号',
  PRIMARY KEY (`id`)
) ENGINE = InnoDB COMMENT = '通知公告表';

设计要点解读

字段设计考量
type字典驱动,支持动态扩展公告类型(通知公告、公司动态、行业咨询、规章制度等)
status控制公告可见性,关闭后不再展示给普通用户
is_important重要通知标识,前端通过红色标签醒目展示,后续可扩展为强制阅读
content使用 text 类型存储富文本 HTML,支持图片、表格等丰富内容
tenant_id多租户隔离,不同企业的公告互不可见
deleted逻辑删除,数据不真正删除,支持审计回溯

表2:system_notice_read — 公告已读关系表

CREATE TABLE `system_notice_read` (
  `id`          bigint      NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `notice_id`   bigint      NOT NULL COMMENT '公告ID',
  `user_id`     bigint      NOT NULL COMMENT '用户ID',
  `read_time`   datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '阅读时间',
  `creator`     varchar(64) NULL DEFAULT '' COMMENT '创建者',
  `create_time` datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater`     varchar(64) NULL DEFAULT '' COMMENT '更新者',
  `update_time` datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `deleted`     bit(1)      NOT NULL DEFAULT b'0' COMMENT '是否删除',
  `tenant_id`   bigint      NOT NULL DEFAULT 0 COMMENT '租户编号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_notice_user` (`notice_id`, `user_id`, `deleted`),
  KEY `idx_notice_id` (`notice_id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE = InnoDB COMMENT = '用户公告已读关系表';

设计要点解读

设计决策理由
独立建表(而非在 notice 表加字段)一条公告对应 N 个用户的已读记录,一对多关系必须独立建表
uk_notice_user 唯一索引防止同一用户对同一公告重复标记已读
read_time 记录阅读时间管理者可追溯"谁在什么时间看了什么",满足企业审计需求
idx_notice_id + idx_user_id 索引优化两个最常见的查询场景:查某公告的已读用户、查某用户的已读公告

为什么是"两张表"而不是更多?

你可能会问:不需要一个 notice_push_record(推送记录表)或者 notice_target(推送目标表)吗?

RuoYi Office 的设计哲学是够用就好,按需扩展

  1. 推送不持久化:WebSocket 推送是实时行为,推送记录可通过操作日志追溯,不需要单独建表
  2. 目标不限定:当前版本公告默认全员可见(通过租户隔离),后续可通过扩展字段(如 target_typetarget_ids)实现定向推送
  3. KISS 原则:Keep It Simple, Stupid. 两张表覆盖 90% 的场景,剩下 10% 的高级需求通过扩展字段解决

后端架构:分层清晰的 Spring Boot 实现

RuoYi Office 后端采用经典的分层架构,通知公告模块的代码组织如下:

yudao-module-system-server/
└── cn.iocoder.yudao.module.system
    ├── controller/admin/notice/
    │   ├── NoticeController.java          # REST API 控制器
    │   └── vo/
    │       ├── NoticePageReqVO.java        # 分页查询请求
    │       ├── NoticeSaveReqVO.java        # 创建/修改请求
    │       └── NoticeRespVO.java           # 响应对象
    ├── service/notice/
    │   ├── NoticeService.java             # 公告 Service 接口
    │   ├── NoticeServiceImpl.java         # 公告 Service 实现
    │   ├── NoticeReadService.java         # 已读 Service 接口
    │   └── NoticeReadServiceImpl.java     # 已读 Service 实现
    └── dal/
        ├── dataobject/notice/
        │   ├── NoticeDO.java              # 公告数据对象
        │   └── NoticeReadDO.java          # 已读数据对象
        └── mysql/notice/
            ├── NoticeMapper.java          # 公告 Mapper
            └── NoticeReadMapper.java      # 已读 Mapper

Controller 层:8 个 API 覆盖全场景

@RestController
@RequestMapping("/system/notice")
public class NoticeController {

    // 创建公告
    @PostMapping("/create")
    @PreAuthorize("@ss.hasPermission('system:notice:create')")
    public CommonResult<Long> createNotice(@Valid @RequestBody NoticeSaveReqVO createReqVO);

    // 修改公告
    @PutMapping("/update")
    @PreAuthorize("@ss.hasPermission('system:notice:update')")
    public CommonResult<Boolean> updateNotice(@Valid @RequestBody NoticeSaveReqVO updateReqVO);

    // 删除公告
    @DeleteMapping("/delete")
    @PreAuthorize("@ss.hasPermission('system:notice:delete')")
    public CommonResult<Boolean> deleteNotice(@RequestParam("id") Long id);

    // 批量删除
    @DeleteMapping("/delete-list")
    @PreAuthorize("@ss.hasPermission('system:notice:delete')")
    public CommonResult<Boolean> deleteNoticeList(@RequestParam("ids") List<Long> ids);

    // 分页查询(含已读状态、创建者名称)
    @GetMapping("/page")
    @PreAuthorize("@ss.hasPermission('system:notice:query')")
    public CommonResult<PageResult<NoticeRespVO>> getNoticePage(@Valid NoticePageReqVO reqVO);

    // 获取公告详情
    @GetMapping("/get")
    @PreAuthorize("@ss.hasPermission('system:notice:query')")
    public CommonResult<NoticeRespVO> getNotice(@RequestParam("id") Long id);

    // 标记已读
    @PostMapping("/mark-read")
    public CommonResult<Boolean> markAsRead(@RequestParam("id") Long id);

    // WebSocket 推送
    @PostMapping("/push")
    @PreAuthorize("@ss.hasPermission('system:notice:update')")
    public CommonResult<Boolean> push(@RequestParam("id") Long id);
}

几个关键设计点

  1. 权限粒度细化:创建、修改、删除、查询分别对应不同的权限标识,可灵活分配给不同角色
  2. 标记已读无需权限markAsRead 不设置 @PreAuthorize,任何登录用户都可以标记自己的已读状态
  3. 分页查询增强getNoticePage 不只返回公告基础信息,还会拼接当前用户的已读状态和创建者名称

Service 层:职责分离的双 Service 设计

通知公告模块将核心业务拆分为两个 Service,职责清晰:

Service职责方法
NoticeService公告的 CRUD创建、修改、删除、分页查询、详情查询
NoticeReadService已读状态管理标记已读、批量标记、查询已读ID、判断是否已读
NoticeServiceImpl — 公告核心逻辑
@Service
public class NoticeServiceImpl implements NoticeService {

    @Resource
    private NoticeMapper noticeMapper;

    @Override
    public Long createNotice(NoticeSaveReqVO createReqVO) {
        NoticeDO notice = BeanUtils.toBean(createReqVO, NoticeDO.class);
        noticeMapper.insert(notice);
        return notice.getId();
    }

    @Override
    public void updateNotice(NoticeSaveReqVO updateReqVO) {
        validateNoticeExists(updateReqVO.getId());
        NoticeDO updateObj = BeanUtils.toBean(updateReqVO, NoticeDO.class);
        noticeMapper.updateById(updateObj);
    }

    @Override
    public void deleteNotice(Long id) {
        validateNoticeExists(id);
        noticeMapper.deleteById(id);
    }

    @VisibleForTesting
    public void validateNoticeExists(Long id) {
        if (id == null) return;
        NoticeDO notice = noticeMapper.selectById(id);
        if (notice == null) {
            throw exception(NOTICE_NOT_FOUND);
        }
    }
}

代码非常简洁,但暗含几个最佳实践

  • BeanUtils.toBean:使用工具类进行 VO → DO 转换,避免手动 set,减少样板代码
  • validateNoticeExists:修改/删除前校验数据是否存在,防止操作幽灵数据
  • @VisibleForTesting:标记为测试可见,方便单元测试直接调用校验方法
  • 统一异常体系:通过 exception(NOTICE_NOT_FOUND) 抛出标准化的业务异常
NoticeReadServiceImpl — 已读状态管理
@Service
public class NoticeReadServiceImpl implements NoticeReadService {

    @Resource
    private NoticeReadMapper noticeReadMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void markAsRead(Long noticeId, Long userId) {
        // 幂等性检查:已读过则不重复插入
        if (noticeReadMapper.isRead(userId, noticeId)) {
            return;
        }
        NoticeReadDO readDO = new NoticeReadDO();
        readDO.setNoticeId(noticeId);
        readDO.setUserId(userId);
        readDO.setReadTime(LocalDateTime.now());
        noticeReadMapper.insert(readDO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void markAsReadBatch(List<Long> noticeIds, Long userId) {
        if (noticeIds == null || noticeIds.isEmpty()) return;
        // 查询已读的公告ID,过滤出未读的
        List<Long> readNoticeIds = noticeReadMapper.selectReadNoticeIds(userId, noticeIds);
        List<Long> unreadNoticeIds = noticeIds.stream()
                .filter(id -> !readNoticeIds.contains(id))
                .toList();
        // 批量插入未读记录
        if (!unreadNoticeIds.isEmpty()) {
            LocalDateTime now = LocalDateTime.now();
            unreadNoticeIds.stream()
                .map(noticeId -> {
                    NoticeReadDO readDO = new NoticeReadDO();
                    readDO.setNoticeId(noticeId);
                    readDO.setUserId(userId);
                    readDO.setReadTime(now);
                    return readDO;
                })
                .forEach(noticeReadMapper::insert);
        }
    }
}

这段代码有两个非常重要的设计:

  1. 幂等性保证markAsRead 先查后插,即使前端重复调用也不会产生重复数据。配合数据库的唯一索引 uk_notice_user,形成了双重保险
  2. 批量优化markAsReadBatch 先批量查询已读状态,再只插入未读的记录,避免了 N 次单条查询

前端实现:Vue3 + Ant Design Vue 的优雅实践

前端代码结构

ruoyi-office-vben/apps/web-antd/src/
├── api/system/notice/
│   └── index.ts                           # API 接口封装
├── views/system/notice/
│   ├── index.vue                          # 通知公告管理页面
│   ├── data.ts                            # 表格列定义 + 表单 Schema
│   └── modules/form.vue                   # 新增/编辑弹窗表单
└── views/dashboard/home/components/notice/
    ├── index.ts                           # 导出入口
    ├── workbench-notice.vue               # 首页通知公告组件
    └── notice-preview-modal.vue           # 公告预览弹窗

API 层:TypeScript 类型安全的接口封装

export namespace SystemNoticeApi {
  export interface Notice {
    id?: number;
    title: string;
    type: number;
    content: string;
    status: number;
    isImportant?: boolean;
    readStatus?: number;       // 已读状态:1已读,0未读
    remark: string;
    creator?: string;
    creatorName?: string;      // 创建者名称
    createTime?: Date;
  }
}

// 分页查询
export function getNoticePage(params: PageParam) {
  return requestClient.get<PageResult<SystemNoticeApi.Notice>>(
    '/system/notice/page', { params }
  );
}

// 标记已读
export function markNoticeAsRead(id: number) {
  return requestClient.post(`/system/notice/mark-read?id=${id}`);
}

// WebSocket 推送
export function pushNotice(id: number) {
  return requestClient.post(`/system/notice/push?id=${id}`);
}

完整的 TypeScript 类型定义确保了前后端数据结构的一致性,readStatuscreatorName 等扩展字段让前端无需二次加工。

管理列表页:声明式配置驱动

RuoYi Office 前端大量采用配置驱动的开发模式,通知公告管理页面的表格和搜索表单都是通过配置生成的:

// 搜索表单 Schema
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'title',
      label: '公告标题',
      component: 'Input',
      componentProps: { placeholder: '请输入公告标题', allowClear: true },
    },
    {
      fieldName: 'type',
      label: '公告类型',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.SYSTEM_NOTICE_TYPE, 'number'),
        placeholder: '请选择公告类型',
        allowClear: true,
      },
    },
    {
      fieldName: 'isImportant',
      label: '是否重要通知',
      component: 'Select',
      componentProps: {
        options: [
          { label: '是', value: true },
          { label: '否', value: false },
        ],
      },
    },
  ];
}

// 表格列定义
export function useGridColumns(): VxeTableGridOptions['columns'] {
  return [
    { type: 'checkbox', width: 40 },
    { field: 'id', title: '公告编号', minWidth: 100 },
    { field: 'title', title: '公告标题', minWidth: 200 },
    {
      field: 'type', title: '公告类型', minWidth: 120,
      cellRender: { name: 'CellDict', props: { type: DICT_TYPE.SYSTEM_NOTICE_TYPE } },
    },
    {
      field: 'isImportant', title: '是否重要通知', minWidth: 120,
      formatter: ({ cellValue }) => cellValue ? '是' : '否',
    },
    {
      field: 'readStatus', title: '已读状态', minWidth: 100,
      cellRender: {
        name: 'VxeTag',
        props: ({ row }) => ({
          type: row.readStatus === 1 ? 'success' : 'warning',
          content: row.readStatus === 1 ? '已读' : '未读',
        }),
      },
    },
    { field: 'createTime', title: '创建时间', formatter: 'formatDateTime' },
    { title: '操作', width: 220, fixed: 'right', slots: { default: 'actions' } },
  ];
}

这种Schema 驱动的开发模式有几个显著优势:

  1. 开发效率高:一个表格 + 搜索表单,只需要定义两个配置数组
  2. 一致性强:所有模块的列表页都遵循相同的模式,用户体验统一
  3. 易维护:新增一个搜索条件或表格列,只需要加一个配置项
  4. 字典集成:通过 DICT_TYPE 自动从字典服务获取公告类型选项,无需硬编码

首页通知组件:工作台的信息枢纽

首页通知公告组件是用户每天打开系统后第一个看到的信息区域:

<script setup lang="ts">
const loading = ref(false);
const noticeList = ref<SystemNoticeApi.Notice[]>([]);

// 未读数量计算
const unreadCount = computed(() => {
  return noticeList.value.filter((notice) => notice.readStatus !== 1).length;
});

// 加载通知列表
async function loadNoticeList() {
  loading.value = true;
  try {
    const response = await getNoticePage({ pageNo: 1, pageSize: 10 });
    noticeList.value = response.list || [];
  } finally {
    loading.value = false;
  }
}

// 页面激活时自动刷新
onMounted(() => loadNoticeList());
onActivated(() => loadNoticeList());
</script>

<template>
  <div class="workbench-notice">
    <!-- 头部:标题 + 未读徽章 + 查看更多 -->
    <div class="notice-header">
      <div class="flex items-center gap-2">
        <h3>通知公告</h3>
        <Badge :count="unreadCount" :overflow-count="99" />
      </div>
      <a @click="handleViewMore">查看更多</a>
    </div>
    <!-- 通知列表 -->
    <div v-for="notice in noticeList" :key="notice.id"
         class="notice-item" @click="handleViewNotice(notice)">
      <!-- 未读标记小圆点 -->
      <div class="h-2 w-2 rounded-full"
           :style="{ backgroundColor: notice.readStatus === 1 ? '#d9d9d9' : '#ff7a00' }" />
      <!-- 类型前缀 -->
      <span class="text-gray-500">{{ `【${getNoticeTypeText(notice.type)}】` }}</span>
      <!-- 标题 -->
      <span class="flex-1 truncate">{{ notice.title }}</span>
      <!-- 重要标签 -->
      <Tag v-if="notice.isImportant" color="red">重要</Tag>
      <!-- 日期 -->
      <span class="text-xs text-gray-400">{{ formatDate(notice.createTime) }}</span>
    </div>
  </div>
</template>

这个组件的交互设计值得学习:

  1. Badge 徽章:头部显示未读数量,数字超过 99 显示 "99+",用户一眼就知道有多少未读
  2. 小圆点视觉:橙色 = 未读,灰色 = 已读,比文字标签更直观
  3. 重要标签:红色 Tag 标记重要通知,在一堆普通通知中脱颖而出
  4. 自动刷新onActivated 钩子确保从其他页面返回工作台时数据自动更新
  5. 点击预览:不跳转页面,弹窗预览详情,减少上下文切换

公告预览弹窗:阅读即已读

预览弹窗是一个巧妙的设计——用户点击查看通知时,自动标记为已读:

<script setup lang="ts">
// 监听弹窗打开,自动标记已读
watch(
  () => props.visible,
  (visible) => {
    if (visible && props.notice?.id && props.notice.readStatus !== 1) {
      markNoticeAsRead(props.notice.id)
        .then(() => emit('refresh'))  // 刷新列表更新已读状态
        .catch(console.error);
    }
  },
);
</script>

<template>
  <Modal :open="visible" :title="modalTitle" width="900px" :footer="null">
    <div v-if="notice" class="notice-preview">
      <!-- 标题 + 标签 -->
      <h2>{{ notice.title }}</h2>
      <Tag color="blue">{{ getNoticeTypeText(notice.type) }}</Tag>
      <Tag v-if="notice.isImportant" color="red">重要</Tag>

      <!-- 元信息:发布人 + 发布时间 -->
      <div class="bg-gray-50 p-3 rounded-lg">
        <span>发布人: {{ notice.creatorName || '-' }}</span>
        <span>发布时间: {{ formatDateTime(notice.createTime) }}</span>
      </div>

      <!-- 富文本内容渲染 -->
      <div class="notice-content" v-html="notice.content || '暂无内容'" />
    </div>
  </Modal>
</template>

"阅读即已读" 的设计,相比需要用户手动点击"已读"按钮的方案,体验更加自然流畅。

技术亮点深度剖析

亮点1:WebSocket 实时推送

传统通知公告的痛点是用户必须主动刷新页面才能看到新公告。RuoYi Office 通过 WebSocket 实现了服务端主动推送

管理员发布公告 → 点击"推送"按钮 → 后端通过 WebSocket 推送 → 在线用户实时收到通知

这意味着:

  • 紧急通知发布后,在线用户立即收到,无需刷新页面
  • 离线用户下次登录时通过首页组件看到未读通知
  • 推送是可选的——管理员可以选择只保存不推送,也可以发布后择机推送

亮点2:字典驱动的类型管理

公告类型不是硬编码的枚举,而是通过字典服务动态管理:

字典类型:system_notice_type
├── 1 → 通知公告
├── 2 → 公司动态
├── 3 → 行业咨询
└── 4 → 规章制度

这带来了极大的灵活性——管理员可以在字典管理中自行添加新的公告类型(如"政策法规"、"培训通知"等),无需修改代码、无需重新部署。

亮点3:多租户天然隔离

得益于 RuoYi Office 的多租户框架,通知公告模块天然支持数据隔离:

  • system_notice 表有 tenant_id 字段
  • system_notice_read 表也有 tenant_id 字段
  • MyBatis Plus 拦截器自动注入租户条件
  • A 公司的公告永远不会出现在 B 公司的界面上

对于 SaaS 模式的企业管理平台,这是一个开箱即用的关键能力。

亮点4:RBAC 权限精细控制

通知公告的每个操作都有独立的权限标识:

操作权限标识典型角色
查询公告system:notice:query全员
创建公告system:notice:create管理员、行政
修改公告system:notice:update管理员
删除公告system:notice:delete超级管理员
标记已读无需权限全员(自己的状态)

这意味着可以灵活配置:比如行政人员可以发布公告但不能删除,普通员工只能查看和标记已读。

扩展思路:从"够用"到"强大"

RuoYi Office 当前的通知公告已经覆盖了大部分企业场景,但如果你有更高级的需求,以下是一些可行的扩展方向:

1. 定向推送

system_notice 表增加 target_typetarget_ids 字段:

ALTER TABLE `system_notice` 
ADD COLUMN `target_type` tinyint DEFAULT 0 COMMENT '推送目标类型(0全员 1部门 2角色 3指定用户)',
ADD COLUMN `target_ids` varchar(2000) DEFAULT NULL COMMENT '推送目标ID列表(JSON数组)';

2. 强制阅读

对重要公告设置强制阅读机制——用户登录后弹出未读的重要公告,必须确认已读后才能进入系统。

3. 阅读统计看板

基于 system_notice_read 表的数据,可以构建阅读统计看板:

  • 公告阅读率(已读人数/总人数)
  • 平均阅读时长
  • 未读用户列表(支持一键提醒)

4. 定时发布

增加 publish_time 字段,支持预设发布时间,配合定时任务在指定时间自动发布并推送。

总结

通知公告看似简单,实则是企业管理系统中信息触达的核心基础设施。RuoYi Office 通过精心的设计,将这个"小功能"做出了企业级的水准:

能力实现方式
内容管理富文本编辑 + 类型分类 + 重要标识 + 状态控制
信息触达WebSocket 实时推送 + 首页组件集成
状态追踪已读/未读记录 + 阅读时间审计
权限管控RBAC 细粒度权限 + 多租户隔离
开发体验Schema 驱动配置 + TypeScript 类型安全
扩展性字典驱动类型 + 预留扩展空间

对于正在开发企业管理系统的团队,或者正在选型的中小企业管理者,RuoYi Office 的通知公告模块提供了一个开箱即用且可深度扩展的参考实现。


想体验完整功能? 访问 RuoYi Office 在线演示 亲自体验通知公告的完整流程。

想深入了解源码? 前端源码:Gitee - ruoyi-office-vben | 后端源码:Gitee - ruoyi-office | GitHub 镜像:GitHub - ruoyi-office

想加入社区交流? 微信添加 17156169080(备注「RuoYi Office」),加入开发者社群,一起打造更好的企业管理平台。