Skip to content

RuoyiOffice之 OA 公文发文管理——套红模板、GB/T 9704 标准预览、审批签发、自动生成收文全流程拆解

🌐 文档地址http://ruoyioffice.com | 📦 源码1ruoyi-office-vben |📦 源码2ruoyi-office |📦 源码3ruoyi-office | 💬 :17156169080(备注「RuoYi Office」)

公文管理是 OA 系统的"皇冠明珠"——看似只是一个文档编辑发布,背后却涉及国家标准的格式规范、多级审批签发、套红模板渲染、PDF 正式文件生成、发文-收文联动等一系列高复杂度设计。很多 OA 系统在这个功能上要么过度简化沦为"发个通知",要么过度复杂让中小企业望而却步。RuoYi Office 的公文发文模块,在符合 GB/T 9704 国家标准的前提下,用精简的 4 张表 + BPM 流程集成 + 实时公文预览,交出了一份"够用且专业"的答卷。

引言:企业公文管理到底难在哪?

如果你在政府机关、国企、大中型企业工作过,一定对"红头文件"不陌生。那个顶部印着红色机关名称、中间一道红色分隔线、下方是正式文号和正文的文件格式,承载着企业最正式、最权威的信息传递。

但当你尝试在 OA 系统中实现公文管理时,会发现这远不是一个"写完标题写正文"的简单功能:

  • 📏 格式标准化:公文格式必须符合 GB/T 9704-2012《党政机关公文格式》,红头、文号、标题、正文、落款的字体、字号、行距都有严格规定
  • 🔴 套红模板:不同机构、不同级别的公文使用不同的红头模板(机关名称、发文字号前缀、印章图片都不同)
  • ✍️ 多级审批签发:公文不是写完就能发——要经过部门审核、领导审批、签发人签发、办公室编号盖章等多个环节
  • 📄 正式文件生成:审批通过后要生成符合国标的正式 PDF 公文文件,带红头、印章、规范排版
  • 📨 发文-收文联动:发文审批通过后,系统要自动为每个收文部门生成收文记录,收文部门需要签收、批示、办理
  • 🔢 文号自动管理:发文字号(如"无办发〔2026〕1号")需要自动拼接、年度递增、不重复

这些需求叠加在一起,就构成了 OA 系统中最具技术含量的模块之一。

RuoYi Office 的公文管理模块基于 Spring Boot 3.5 + Flowable 7 + Vue3 + Vben Admin 技术栈,参考 GB/T 9704-2012 标准,设计了一套面向中小企业和国企科室的公文管理体系。本文聚焦公文发文这个核心子模块,从业务设计到技术实现进行完整拆解。


一、业务设计:公文发文在整个公文体系中的位置

1.1 公文管理四大模块

RuoYi Office 的公文管理由四个子模块组成,各司其职:

公文管理四大模块架构 - 套红模板、公文发文、公文收文、外部收文及其关联关系

▲ 公文管理四大模块:套红模板提供红头配置,公文发文审批通过后自动按主送部门生成收文记录

模块类型核心职责
套红模板基础数据(无 BPM)管理公文红头模板:机关名称、字号前缀、印章图片、分隔线样式
公文发文BPM 流程表单起草、审批、签发、编号发文全流程,含公文实时预览与 PDF 生成
公文收文BPM 流程表单内部收文签收、领导批示、承办部门办理、办结归档
外部收文BPM 流程表单外来公文登记、批示、办理、归档(独立于内部发文体系)

1.2 发文与其他模块的关联关系

发文是整个公文体系的起点,其核心联动逻辑包括:

  • 套红模板 → 发文:发文时选择模板,系统根据模板配置渲染公文预览和生成正式 PDF
  • 发文 → 收文:发文审批通过后,系统根据主送部门自动创建收文记录,收文通过 sendBillId 关联原始发文
  • 收文 → 发文:收文详情页可跳转查看关联的原始发文及公文预览

这种"一次发文、多方收文"的联动设计,真实还原了企业公文流转的业务本质——发文是"一对多"的信息下达。


二、流程设计:从起草到签发的五级审批

公文发文不同于普通的审批单据(请假、用印等),它多了"签发"和"编号发文"两个关键环节——这两个环节直接决定了公文的正式性和法律效力。

2.1 审批流程节点

起草人起草 → 部门负责人审核 → 分管领导审批 → 签发人签发 → 办公室编号发文 → 结束(自动生成收文)
节点角色核心操作设计考量
起草人起草发起人填写表单、编辑正文、预览公文效果公文的源头,需要套红模板选择
部门负责人审核部门负责人审核公文内容是否准确、合规第一道关卡,确保内容质量
分管领导审批分管领导审批公文(可配置会签)可根据密级条件分支增加保密审查
签发人签发主要领导最终签发,自动记录签发人信息签发人姓名会显示在正式公文上
办公室编号发文办公室文秘编号盖章,生成正式 PDF文秘可编辑文号序号,确认后生成正式文件

2.2 流程中的业务逻辑定制

RuoYi Office 的公文发文流程不只是简单的"通过/驳回",还在关键节点嵌入了精准的业务逻辑:

签发人签发节点:审批人点击"通过"前,系统自动将当前审批人的信息赋值为签发人,并保存到业务表中——签发人姓名会出现在正式公文的落款区域。

办公室编号发文节点:这是整个流程中最特殊的节点:

  1. 放开文号编辑:在其他节点中文号字段只读,只有此节点可以编辑发文字号、年份、序号
  2. 强制生成 PDF:审批人必须先点击"生成正式公文"按钮生成 PDF,才能点击"通过"
  3. 自动生成收文:审批通过后,系统自动为每个主送部门和抄送部门创建收文记录
typescript
async function beforeApproval(): Promise<boolean> {
  // 签发人签发节点:自动保存签发人信息
  if (props.nodeKeyName === '签发人签发') {
    if (!formData.value.signerId) {
      formData.value.signerId = userStore.userInfo?.id;
      formData.value.signerName = userStore.userInfo?.nickname || '';
    }
    await saveOfficeDocSend(formData.value);
  }

  // 办公室编号发文节点:校验 PDF 已生成
  if (props.nodeKeyName === '办公室编号发文' && basicFormRef.value) {
    const formValues = await basicFormRef.value.getFormValues(false);
    await saveOfficeDocSend({ ...formData.value, ...formValues });

    if (!formData.value.docFileUrl) {
      message.warning('请先生成正式公文PDF后再通过审批');
      return false;
    }
  }
  return true;
}

这种节点级别的业务定制是 RuoYi Office BPM 架构的核心优势——不同审批节点可以有完全不同的业务逻辑,而框架提供了统一的 beforeApproval 钩子机制来实现。


三、功能实现:发文表单与实时公文预览

3.1 左右分栏布局:表单 + 预览

公文发文最大的交互创新是采用了左右分栏布局——左侧是表单填写区,右侧是公文实时预览面板。用户在左侧填写标题、选择模板、输入正文的同时,右侧实时渲染出符合 GB/T 9704 标准的公文预览效果。

▲ 公文发文详情页:左侧填写表单(套红模板、标题、密级、文号、主送部门、正文、附件),右侧实时预览公文效果

布局组件 DocPreviewLayout 的实现非常简洁:

vue
<template>
  <div class="doc-preview-layout">
    <div class="doc-form-area">
      <slot name="form"></slot>
    </div>
    <div class="doc-preview-area">
      <slot name="preview"></slot>
    </div>
  </div>
</template>

<style scoped>
.doc-preview-layout {
  display: flex;
  gap: 12px;
  width: 100%;
}
.doc-form-area {
  flex: 1 1 55%;
  min-width: 0;
}
.doc-preview-area {
  flex: 0 0 380px;
  max-width: 420px;
}
/* 小屏幕自动切换为上下排列 */
@media (width <= 1200px) {
  .doc-preview-layout { flex-direction: column; }
  .doc-preview-area { flex: none; max-width: 100%; }
}
</style>

在发文表单中的使用:

vue
<DocPreviewLayout>
  <template #form>
    <BasicForm ... @values-change="handleFormValuesChange" />
    <!-- 公文正文编辑区 -->
    <!-- 附件上传区 -->
  </template>
  <template #preview>
    <DocPreview
      :template-data="templateData"
      :form-data="formData"
      :can-generate-pdf="props.nodeKeyName === '办公室编号发文'"
      @generate-pdf="handleGeneratePdf"
    />
  </template>
</DocPreviewLayout>

3.2 表单字段设计

公文发文表单的字段设计兼顾了 GB/T 9704 标准要素和实际操作便利性:

字段组件说明
套红模板HelpInput + 弹窗选择点击弹出模板选择列表,双击或确认选择模板
标题Input(全宽)公文标题,显示在公文预览的标题区域
字号 / 年份 / 序号Input + InputNumber仅在"办公室编号发文"节点可编辑
密级 / 紧急程度 / 公开类别Select(字典驱动)通过字典服务动态获取选项
发文日期DatePicker默认当天
发文部门HelpInput + 部门选择弹窗从组织架构树中选择
主送部门HelpInput + 多选部门弹窗支持多选,审批通过后按部门生成收文
抄送部门HelpInput + 多选部门弹窗可选,生成抄送类型的收文记录
签发人Input(自动填充)签发人签发节点自动带入当前审批人
公文正文Textarea正文内容,实时反映到右侧预览
附件AttachmentList支持上传附件(最多10个,单个20MB)

套红模板选择弹窗是一个亮点设计——支持搜索筛选、单选、双击快速选择。选中模板后,表单自动填充模板名称和发文字号前缀,右侧预览立即渲染出对应模板的红头效果。

3.3 发文列表页

▲ 公文发文列表页:展示单据编号、公文标题、发文字号、密级、紧急程度、发文部门、主送部门、流程状态

列表页遵循 RuoYi Office 的统一业务单据列表模式:

  • 搜索区:支持按公文标题、发文字号、流程状态筛选
  • 工具栏:新建发文、导出、批量删除(仅未提交/已撤回的单据可删除)
  • 单据编号链接:点击编号跳转到详情页,便于快速查看
  • 字典渲染:密级、紧急程度、流程状态均通过字典标签渲染

四、公文预览:GB/T 9704 标准的前端渲染

4.1 预览组件架构

DocPreview 组件是公文发文模块的核心创新。它根据套红模板配置 + 表单数据,实时渲染出符合 GB/T 9704 标准的公文预览效果:

+──────────────────────────────────────+
│       [机关名称 — 红色大字]          │  ← 来自套红模板 orgName
│              文  件                  │
│    ══════════════════════════        │  ← 红色分隔线(单线/双线可配置)
│       字号〔年份〕序号号              │  ← 自动拼接
│                                      │
│           [公文标题]                  │  ← 2号小标宋
│                                      │
│  主送机关:xxx                       │  ← mainReceiveDeptNames
│                                      │
│       [正文内容...]                  │  ← 3号仿宋
│                                      │
│                  [机关名称]  [印章]   │  ← orgName + sealImage
│                  签发人:xxx          │  ← signerName
│                  2026年3月22日        │  ← sendDate
│                                      │
│  ────────────────────────────        │
│  抄送:xxx                           │  ← ccDeptNames
│  ────────────────────────────        │
+──────────────────────────────────────+

4.2 实时预览的响应式实现

预览组件通过 Vue computed 属性实时响应表单数据变化:

typescript
const orgName = computed(() => props.templateData?.orgName || '机关名称');
const orgFontSize = computed(() => props.templateData?.orgNameFontSize || 36);
const isSingleLine = computed(
  () => (props.templateData?.headerSeparatorType ?? 1) === 1,
);

const docNumber = computed(() => {
  const { docMark, docYear, docSequence } = props.formData || {};
  if (docMark && docYear && docSequence) {
    return `${docMark}〔${docYear}〕${docSequence}号`;
  }
  return '';
});

const formattedDate = computed(() => {
  const raw = props.formData?.sendDate;
  if (!raw) return '';
  const parts = raw.split('-');
  if (parts.length === 3) {
    return `${parts[0]}年${Number.parseInt(parts[1]!, 10)}月${Number.parseInt(parts[2]!, 10)}日`;
  }
  return raw;
});

设计亮点

  • 发文字号自动拼接为中文格式:字号〔年份〕序号号(如"无办发〔2026〕1号")
  • 日期自动转换为中文格式:2026-03-222026年3月22日
  • 机关名称字号可配置(模板中的 orgNameFontSize),预览按比例缩放
  • 分隔线样式可配置(单线/双线),忠实还原 GB/T 9704 两种标准样式

4.3 双模预览:侧栏预览 + 标准预览弹窗

▲ 标准预览弹窗:按 A4 纸张 72dpi(595×842px)比例渲染,正文仿宋3号16px、标题小标宋2号22px

DocPreview 提供了两种预览模式:

预览模式场景技术实现
侧栏预览填写表单时实时查看效果100% 宽度自适应,按比例缩放字号
标准预览弹窗查看 1:1 国标排版效果固定 A4 尺寸(595×842px),标准字体字号

标准预览弹窗严格按照 GB/T 9704 排版参数:

css
.std-page {
  width: 595px;
  min-height: 842px;
  padding: 105px 74px 99px 79px;       /* GB/T 9704 页边距 */
  font-family: fangsong, '仿宋', serif; /* 正文仿宋体 */
  font-size: 16px;                      /* 3号字 ≈ 16px */
  line-height: 28.6px;                  /* 行距 28.6px */
}

.std-title {
  font-family: '小标宋体', SimSun, serif;
  font-size: 22px;                      /* 2号字 ≈ 22px */
  font-weight: bold;
}

.std-header-org {
  color: #c00;                          /* 红色机关名称 */
  letter-spacing: 0.15em;
}

五、表结构设计

5.1 ER 关系总览

┌───────────────────────────────────┐
│    oa_office_doc_template         │
│    (套红模板表)                    │
│  ├ template_name (模板名称)       │
│  ├ org_name (机关/公司名称)       │
│  ├ org_name_font_size (字号)      │
│  ├ doc_mark_prefix (字号前缀)     │
│  ├ seal_image_url (印章图片)      │
│  └ header_separator_type (分隔线) │
└──────────────┬────────────────────┘
               │ 1 : N

┌───────────────────────────────────┐
│    oa_office_doc_send             │
│    (公文发文表)                    │
│  ├ template_id → 模板             │
│  ├ title (公文标题)               │
│  ├ doc_number (完整发文字号)       │
│  ├ doc_mark / doc_year / doc_seq  │
│  ├ content (正文 HTML)            │
│  ├ main_receive_dept_ids/names    │
│  ├ cc_dept_ids/names              │
│  ├ signer_id / signer_name       │
│  ├ doc_file_url (PDF 文件)        │
│  └ process_instance_id (流程)     │
└──────────────┬────────────────────┘
               │ 1 : N (审批通过自动生成)

┌───────────────────────────────────┐
│    oa_office_doc_receive          │
│    (公文收文表)                    │
│  ├ send_bill_id → 关联发文        │
│  ├ receive_dept_id/name           │
│  ├ handle_status (办理状态)       │
│  └ ...                            │
└───────────────────────────────────┘

5.2 公文发文核心字段

字段类型说明设计考量
template_idbigint套红模板 ID关联模板,决定公文红头样式
titlevarchar(200)公文标题GB/T 9704 核心要素
doc_markvarchar(50)发文字号标识如"无办发",可从模板带入
doc_yearint年份如 2026
doc_sequenceint文号序号年度内递增
doc_numbervarchar(100)完整发文字号自动拼接:"无办发〔2026〕1号"
secret_leveltinyint密级0-公开 1-内部 2-秘密 3-机密 4-绝密
urgency_leveltinyint紧急程度0-普通 1-急件 2-特急
public_categorytinyint公开类别0-主动公开 1-依申请公开 2-不予公开
main_receive_dept_idsvarchar(500)主送部门 IDsJSON 数组,审批通过后按此生成收文
main_receive_dept_namesvarchar(500)主送部门名称显示在公文预览的"主送"区域
cc_dept_ids / cc_dept_namesvarchar(500)抄送部门可选,生成抄送类型收文
contenttext公文正文富文本 HTML
signer_id / signer_namebigint/varchar签发人签发节点自动填充
doc_file_urlvarchar(500)正式公文 PDF办公室编号发文节点生成
process_instance_idvarchar(64)流程实例 IDBPM 集成标准字段
process_statustinyint流程状态BPM 集成标准字段

5.3 套红模板核心字段

字段类型说明
template_namevarchar(100)模板名称(如"总公司正式发文模板")
org_namevarchar(200)机关/公司名称(红头显示的文字)
org_name_font_sizeint机关名称字号(默认 36)
doc_mark_prefixvarchar(50)发文字号前缀(如"无办发")
seal_image_urlvarchar(500)印章图片 URL
header_separator_typetinyint分隔线样式(1-单线 2-双线)

为什么把套红模板设计为独立表而不是在发文表中直接存储? 因为同一个模板会被多次复用——企业可能有"总公司发文模板"、"分公司发文模板"、"党委发文模板"等多个模板,独立管理便于复用和统一修改。


六、核心后端代码解析

6.1 OfficeDocSendServiceImpl:BPM 流程表单的典型实现

公文发文服务实现了 FlowBillService<OaBillTypeEnum> 接口,这是 RuoYi Office BPM 架构的标准接入方式:

java
@Service
@Validated
public class OfficeDocSendServiceImpl
        implements OfficeDocSendService, FlowBillService<OaBillTypeEnum> {

    @Resource
    private OfficeDocSendMapper officeDocSendMapper;
    @Resource
    private OfficeDocReceiveMapper officeDocReceiveMapper;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Resource
    private AttachmentService attachmentService;

    @Override
    public Long submitOfficeDocSend(OfficeDocSendSaveReqVO saveReqVO) {
        // 1. 自动生成单据编号
        if (StringUtils.isBlank(saveReqVO.getBillCode())) {
            saveReqVO.setBillCode(BillCodeUtils.generateBillCode(
                SystemEnum.OA, OaBillTypeEnum.OA_OFFICE_DOC_SEND));
        }
        // 2. 自动拼接发文字号
        buildDocNumber(saveReqVO);

        // 3. 保存业务数据
        OfficeDocSendDO docSend = BeanUtils.toBean(saveReqVO, OfficeDocSendDO.class)
                .setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus());
        officeDocSendMapper.insertOrUpdate(docSend);

        // 4. 构建流程变量(密级、紧急程度等可用于条件分支)
        Map<String, Object> processVariables =
            BpmProcessVariableUtils.buildBillVariables(saveReqVO);
        processVariables.put(PV_DOC_SECRET_LEVEL, saveReqVO.getSecretLevel());
        processVariables.put(PV_DOC_URGENCY_LEVEL, saveReqVO.getUrgencyLevel());
        processVariables.put(BpmProcessVariableConstants.CAUSE, saveReqVO.getTitle());

        // 5. 发起流程实例
        String processInstanceId = processInstanceApi.submitProcessInstance(
            Long.valueOf(saveReqVO.getCreator()),
            new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(
                    OaBillTypeEnum.OA_OFFICE_DOC_SEND.getProcessDefinitionKey())
                .setVariables(processVariables)
                .setBusinessKey(String.valueOf(docSend.getId()))
        ).getCheckedData();

        // 6. 回写流程实例 ID
        officeDocSendMapper.updateById(
            new OfficeDocSendDO().setId(docSend.getId())
                .setProcessInstanceId(processInstanceId));

        // 7. 保存附件
        if (saveReqVO.getAttachments() != null) {
            attachmentService.saveAttachmentList(
                OaBillTypeEnum.OA_OFFICE_DOC_SEND.getTypeCode(),
                docSend.getId(), saveReqVO.getAttachments());
        }
        return docSend.getId();
    }
}

关键设计解读

  1. buildDocNumber():自动拼接发文字号 doc_mark + "〔" + doc_year + "〕" + doc_sequence + "号"
  2. 流程变量传递:密级和紧急程度作为流程变量传入,可在流程设计器中配置条件分支(如密级 >= 秘密时增加保密审查节点)
  3. insertOrUpdate():支持"暂存"再"提交"的两步操作

6.2 发文审批通过 → 自动生成收文

这是公文发文最核心的业务联动逻辑。当发文审批全部通过后,BPM 框架通过事件驱动回调 onProcessApproved 方法:

java
@Override
public void onProcessApproved(String businessKey) {
    Long sendId = Long.parseLong(businessKey);
    OfficeDocSendDO sendBill = officeDocSendMapper.selectById(sendId);
    if (sendBill == null) return;

    // 主送部门:生成需要办理的收文记录
    if (StringUtils.isNotBlank(sendBill.getMainReceiveDeptIds())) {
        generateReceiveRecords(sendBill,
            sendBill.getMainReceiveDeptIds(),
            sendBill.getMainReceiveDeptNames(),
            RECEIVE_TYPE_MAIN);
    }

    // 抄送部门:生成仅需知悉的收文记录
    if (StringUtils.isNotBlank(sendBill.getCcDeptIds())) {
        generateReceiveRecords(sendBill,
            sendBill.getCcDeptIds(),
            sendBill.getCcDeptNames(),
            RECEIVE_TYPE_CC);
    }
}

private void generateReceiveRecords(OfficeDocSendDO sendBill,
        String deptIdsStr, String deptNamesStr, int receiveType) {
    String[] deptIds = deptIdsStr.split(",");
    String[] deptNames = deptNamesStr.split("、");

    // 同步复制发文附件到收文
    List<AttachmentDO> sendAttachments = attachmentService
        .getAttachmentListByBusiness(
            OaBillTypeEnum.OA_OFFICE_DOC_SEND.getTypeCode(),
            sendBill.getId());

    for (int i = 0; i < deptIds.length; i++) {
        // 创建收文记录,关联原始发文
        OfficeDocReceiveDO receiveDO = new OfficeDocReceiveDO();
        receiveDO.setBillCode(BillCodeUtils.generateBillCode(
            SystemEnum.OA, OaBillTypeEnum.OA_OFFICE_DOC_RECEIVE));
        receiveDO.setSendBillId(sendBill.getId());
        receiveDO.setTemplateId(sendBill.getTemplateId());
        receiveDO.setReceiveType(receiveType);
        receiveDO.setDocNumber(sendBill.getDocNumber());
        receiveDO.setTitle(sendBill.getTitle());
        receiveDO.setContent(sendBill.getContent());
        receiveDO.setDocFileUrl(sendBill.getDocFileUrl());
        receiveDO.setReceiveDeptId(Long.parseLong(deptIds[i].trim()));
        receiveDO.setReceiveDeptName(deptNames[i].trim());
        receiveDO.setHandleStatus(0); // 待签收
        receiveDO.setRemark("由发文单[" + sendBill.getBillCode()
            + "]审批通过后自动生成"
            + (receiveType == RECEIVE_TYPE_CC ? "(抄送)" : ""));

        officeDocReceiveMapper.insert(receiveDO);
        // 清除自动填充的 creator,收文需要手动签收认领
        officeDocReceiveMapper.update(null,
            Wrappers.<OfficeDocReceiveDO>update()
                .set("creator", null)
                .eq("id", receiveDO.getId()));

        // 复制附件到收文
        if (CollUtil.isNotEmpty(sendAttachments)) {
            List<AttachmentSaveReqVO> copyList =
                BeanUtils.toBean(sendAttachments, AttachmentSaveReqVO.class);
            copyList.forEach(att -> att.setId(null));
            attachmentService.saveAttachmentList(
                OaBillTypeEnum.OA_OFFICE_DOC_RECEIVE.getTypeCode(),
                receiveDO.getId(), copyList);
        }
    }
}

这段代码的设计亮点

  1. 按部门拆分:一条发文可能主送3个部门、抄送2个部门,系统为每个部门各生成一条独立的收文记录,各部门可以独立签收、批示、办理
  2. 区分主送/抄送:主送收文需要走完整的签收→批示→办理→归档流程,抄送收文仅需知悉
  3. 关联原始发文:收文通过 sendBillId 关联原始发文,可回溯查看原文和公文预览
  4. 附件自动同步:发文的附件自动复制到每条收文记录中
  5. 清除 creator:收文不指定创建人,由收文部门人员手动签收认领

6.3 OfficeDocPdfService:服务端 PDF 生成

公文 PDF 的生成采用 Thymeleaf 模板引擎 + OpenHTMLtoPDF 方案,在服务端完成标准化渲染:

java
@Service
public class OfficeDocPdfService {

    @Resource
    private TemplateEngine templateEngine;
    @Resource
    private FileApi fileApi;

    public String generateDocPdf(Long sendId) {
        OfficeDocSendDO send = officeDocSendMapper.selectById(sendId);
        OfficeDocTemplateDO template = officeDocTemplateMapper
            .selectById(send.getTemplateId());

        // 1. 使用 Thymeleaf 渲染 HTML
        String html = renderHtml(send, template);

        // 2. OpenHTMLtoPDF 转换为 PDF(注册中文字体)
        byte[] pdfBytes = convertHtmlToPdf(html);

        // 3. 上传文件存储,返回 URL
        String fileName = send.getDocNumber() + ".pdf";
        String url = fileApi.createFile(pdfBytes, fileName,
            "officedoc", "application/pdf");

        // 4. 回写 PDF URL 到发文记录
        officeDocSendMapper.updateById(
            new OfficeDocSendDO().setId(sendId).setDocFileUrl(url));
        return url;
    }

    private byte[] convertHtmlToPdf(String html) {
        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
            PdfRendererBuilder builder = new PdfRendererBuilder();
            builder.useFastMode();
            registerChineseFonts(builder); // 注册仿宋、宋体、黑体
            builder.withHtmlContent(html, null);
            builder.toStream(os);
            builder.run();
            return os.toByteArray();
        }
    }

    private void registerChineseFonts(PdfRendererBuilder builder) {
        String fontDir = System.getProperty("os.name", "")
            .toLowerCase().contains("win")
                ? "C:/Windows/Fonts/" : "/usr/share/fonts/truetype/";

        // 仿宋 — 公文正文字体
        tryRegisterFont(builder, fontDir + "simfang.ttf",
            "fangsong", "仿宋", "STFangsong");
        // 宋体 — 标题、机关名称字体
        tryRegisterFont(builder, fontDir + "simsun.ttc",
            "SimSun", "宋体", "小标宋体", "serif");
    }
}

技术选型考量

方案优势劣势选择理由
前端 html2pdf.js所见即所得字体依赖客户端、分页控制差
后端 iText功能强大商业许可、API 复杂
后端 OpenHTMLtoPDF开源免费、CSS 支持好、字体可控需要注册中文字体

选择后端生成 PDF 的关键原因是字体一致性——公文的仿宋、小标宋等字体在不同客户端可能缺失,而服务端通过注册系统字体可以确保输出的 PDF 在任何环境下显示一致。


七、FlowBillService:业务模块接入 BPM 的标准范式

公文发文服务实现的 FlowBillService 接口,是 RuoYi Office 所有 BPM 流程表单的统一接入契约。理解了这个接口,就理解了 RuoYi Office 的业务模块如何与流程引擎解耦协作:

java
public interface FlowBillService<T extends BillTypeEnum> {

    /** 获取支持的单据类型 */
    T getSupportedBillType();

    /** 流程状态变更回调 — 必须实现 */
    void updateProcessStatus(String businessKey, Integer status);

    /** 流程审批通过后的业务处理 — 按需重写 */
    default void onProcessApproved(String businessKey) { }

    /** 流程拒绝后的业务处理 — 按需重写 */
    default void onProcessRejected(String businessKey) { }

    /** 删除业务单据 — 按需重写 */
    default void deleteBill(String businessKey) { }
}

对于公文发文,实现如下:

方法实现逻辑
getSupportedBillType()返回 OaBillTypeEnum.OA_OFFICE_DOC_SEND
updateProcessStatus()更新发文单的 processStatus 字段
onProcessApproved()核心! 遍历主送/抄送部门,自动生成收文记录
deleteBill()删除发文记录及关联附件

事件驱动流转全景

Flowable 引擎 → BpmProcessInstanceEventListener → Spring ApplicationEvent
    → OaLocalNotificationListener → OaFlowBillServiceFactory
    → OfficeDocSendServiceImpl.updateProcessStatus()
    → OfficeDocSendServiceImpl.onProcessApproved() ← 自动生成收文

只需一个接口 + 四个方法,公文发文就完成了与 BPM 的全部集成。新增其他类型的业务单据(如合同审批、采购申请),也只需实现这同一个接口。这就是架构的力量。


八、字典数据:灵活可扩展的枚举管理

公文发文涉及的所有枚举值均通过字典服务管理,而非硬编码:

字典类型字典值使用场景
office_doc_secret_level公开、内部、秘密、机密、绝密密级选择 + 流程条件分支
office_doc_urgency_level普通、急件、特急紧急程度标识
office_doc_public_category主动公开、依申请公开、不予公开公开类别标识

字典驱动的好处:管理员可以在系统后台自行添加、修改字典值(如增加"内部"级别),无需修改代码重新部署。前端通过 getDictOptions('office_doc_secret_level', 'number') 自动获取最新选项。


九、总结:公文发文模块的技术亮点

能力实现方式价值
国标合规渲染严格遵循 GB/T 9704-2012输出的公文格式经得起检查
套红模板独立配置,一键切换红头/印章/字号前缀多机构、多级别公文复用
实时预览Vue computed + 左右分栏布局所填即所见,体验流畅
PDF 生成服务端 Thymeleaf + OpenHTMLtoPDF字体一致、输出可控
多级审批Flowable 7 + 节点级业务定制签发自动记录、编号前强制生成 PDF
发文-收文联动onProcessApproved 自动生成真实还原企业公文流转
事件驱动解耦FlowBillService + Spring Event业务模块与 BPM 零耦合
字典驱动密级/紧急程度/公开类别均为字典零代码扩展枚举值
多租户隔离全表带 tenant_idSaaS 场景开箱即用

十、快速体验

在线体验

  • 演示地址http://ruoyioffice.com/web/
  • 账号密码:admin / admin123
  • 操作路径:OA 协同办公 → 公文管理 → 公文发文

源码获取

平台仓库地址
GitCode(后端)https://gitcode.com/zhouzhongyan/ruoyi-office.git
GitCode(前端)https://gitcode.com/zhouzhongyan/ruoyi-office-vben.git
GitHub(后端)https://github.com/yuqing2026/ruoyi-office.git

技术栈速览

层次技术版本
后端框架Spring Boot / Spring Cloud3.5
流程引擎Flowable7.0.1
PDF 生成OpenHTMLtoPDF + Thymeleaf最新版
前端框架Vue 3 + TypeScript3.5
UI 框架Vben Admin + Ant Design Vue5.x / 4.x

结语

公文管理不是"发个通知"那么简单。 从套红模板的格式配置,到 GB/T 9704 标准的排版渲染,从多级审批签发的流程编排,到发文-收文的自动联动——每一个环节都需要精心设计。RuoYi Office 的公文发文模块,用精简的 4 张表、标准的 FlowBillService 接口、创新的左右分栏预览布局,在"专业合规"和"够用易用"之间找到了平衡点。

如果你正在为企业 OA 系统选型,或者需要一套开箱即用且可深度定制的公文管理方案,RuoYi Office 值得一试。


💡 觉得有价值?

Star 仓库https://gitee.com/yqzy1688/ruoyi-office

💬 技术交流:添加微信 17156169080,备注「RuoYi Office」,加入技术交流群

📚 更多文章http://ruoyioffice.com