RuoyiOffice之 OA 公文发文管理——套红模板、GB/T 9704 标准预览、审批签发、自动生成收文全流程拆解
🌐 文档地址:http://ruoyioffice.com | 📦 源码1:ruoyi-office-vben |📦 源码2:ruoyi-office |📦 源码3:ruoyi-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 的公文发文流程不只是简单的"通过/驳回",还在关键节点嵌入了精准的业务逻辑:
签发人签发节点:审批人点击"通过"前,系统自动将当前审批人的信息赋值为签发人,并保存到业务表中——签发人姓名会出现在正式公文的落款区域。
办公室编号发文节点:这是整个流程中最特殊的节点:
- 放开文号编辑:在其他节点中文号字段只读,只有此节点可以编辑发文字号、年份、序号
- 强制生成 PDF:审批人必须先点击"生成正式公文"按钮生成 PDF,才能点击"通过"
- 自动生成收文:审批通过后,系统自动为每个主送部门和抄送部门创建收文记录
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 的实现非常简洁:
<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>在发文表单中的使用:
<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 属性实时响应表单数据变化:
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-22→2026年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 排版参数:
.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_id | bigint | 套红模板 ID | 关联模板,决定公文红头样式 |
title | varchar(200) | 公文标题 | GB/T 9704 核心要素 |
doc_mark | varchar(50) | 发文字号标识 | 如"无办发",可从模板带入 |
doc_year | int | 年份 | 如 2026 |
doc_sequence | int | 文号序号 | 年度内递增 |
doc_number | varchar(100) | 完整发文字号 | 自动拼接:"无办发〔2026〕1号" |
secret_level | tinyint | 密级 | 0-公开 1-内部 2-秘密 3-机密 4-绝密 |
urgency_level | tinyint | 紧急程度 | 0-普通 1-急件 2-特急 |
public_category | tinyint | 公开类别 | 0-主动公开 1-依申请公开 2-不予公开 |
main_receive_dept_ids | varchar(500) | 主送部门 IDs | JSON 数组,审批通过后按此生成收文 |
main_receive_dept_names | varchar(500) | 主送部门名称 | 显示在公文预览的"主送"区域 |
cc_dept_ids / cc_dept_names | varchar(500) | 抄送部门 | 可选,生成抄送类型收文 |
content | text | 公文正文 | 富文本 HTML |
signer_id / signer_name | bigint/varchar | 签发人 | 签发节点自动填充 |
doc_file_url | varchar(500) | 正式公文 PDF | 办公室编号发文节点生成 |
process_instance_id | varchar(64) | 流程实例 ID | BPM 集成标准字段 |
process_status | tinyint | 流程状态 | BPM 集成标准字段 |
5.3 套红模板核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
template_name | varchar(100) | 模板名称(如"总公司正式发文模板") |
org_name | varchar(200) | 机关/公司名称(红头显示的文字) |
org_name_font_size | int | 机关名称字号(默认 36) |
doc_mark_prefix | varchar(50) | 发文字号前缀(如"无办发") |
seal_image_url | varchar(500) | 印章图片 URL |
header_separator_type | tinyint | 分隔线样式(1-单线 2-双线) |
为什么把套红模板设计为独立表而不是在发文表中直接存储? 因为同一个模板会被多次复用——企业可能有"总公司发文模板"、"分公司发文模板"、"党委发文模板"等多个模板,独立管理便于复用和统一修改。
六、核心后端代码解析
6.1 OfficeDocSendServiceImpl:BPM 流程表单的典型实现
公文发文服务实现了 FlowBillService<OaBillTypeEnum> 接口,这是 RuoYi Office BPM 架构的标准接入方式:
@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();
}
}关键设计解读:
buildDocNumber():自动拼接发文字号doc_mark + "〔" + doc_year + "〕" + doc_sequence + "号"- 流程变量传递:密级和紧急程度作为流程变量传入,可在流程设计器中配置条件分支(如密级 >= 秘密时增加保密审查节点)
insertOrUpdate():支持"暂存"再"提交"的两步操作
6.2 发文审批通过 → 自动生成收文
这是公文发文最核心的业务联动逻辑。当发文审批全部通过后,BPM 框架通过事件驱动回调 onProcessApproved 方法:
@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);
}
}
}这段代码的设计亮点:
- 按部门拆分:一条发文可能主送3个部门、抄送2个部门,系统为每个部门各生成一条独立的收文记录,各部门可以独立签收、批示、办理
- 区分主送/抄送:主送收文需要走完整的签收→批示→办理→归档流程,抄送收文仅需知悉
- 关联原始发文:收文通过
sendBillId关联原始发文,可回溯查看原文和公文预览 - 附件自动同步:发文的附件自动复制到每条收文记录中
- 清除 creator:收文不指定创建人,由收文部门人员手动签收认领
6.3 OfficeDocPdfService:服务端 PDF 生成
公文 PDF 的生成采用 Thymeleaf 模板引擎 + OpenHTMLtoPDF 方案,在服务端完成标准化渲染:
@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 的业务模块如何与流程引擎解耦协作:
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_id | SaaS 场景开箱即用 |
十、快速体验
在线体验
- 演示地址: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 Cloud | 3.5 |
| 流程引擎 | Flowable | 7.0.1 |
| PDF 生成 | OpenHTMLtoPDF + Thymeleaf | 最新版 |
| 前端框架 | Vue 3 + TypeScript | 3.5 |
| UI 框架 | Vben Admin + Ant Design Vue | 5.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