【流程】出差、费用报销
出差、费用报销,由 yudao-module-oa 后端模块的 travel、reimbursement 包实现,前端实现在 @/views/oa/travel/apply、@/views/oa/travel/reimbursement、@/views/oa/reimbursement 目录。
出差申请记录计划行程,审批通过后写入出差考勤;差旅报销记录实际费用,可关联已通过的出差申请;普通费用报销用于其他费用。三类单据分别保存、各自走 BPM,费用报销比出差单据更严格(驳回后不能再改)。
本文涉及表如下图所示:
# 1. 出差申请
出差申请,由 OaTravelApplyController 提供接口(/oa/travel-apply)。
# 1.1 表结构
省略 creator/create_time/updater/update_time/deleted/tenant_id 等通用字段
出差申请表 oa_travel_apply,保存出差事由、起止日期、同行人、行程和预计费用:
CREATE TABLE `oa_travel_apply` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`no` varchar(64) NOT NULL COMMENT '单据编号',
`dept_id` bigint DEFAULT NULL COMMENT '申请部门编号',
`reason` text COMMENT '出差事由',
`start_time` datetime DEFAULT NULL COMMENT '开始日期',
`end_time` datetime DEFAULT NULL COMMENT '结束日期',
`days` int DEFAULT NULL COMMENT '出差天数',
`companion` varchar(500) DEFAULT NULL COMMENT '同行人',
`estimated_price` decimal(18,2) DEFAULT NULL COMMENT '预计费用,元',
`reimburse_status` tinyint NOT NULL DEFAULT '0' COMMENT '报销状态',
`status` tinyint NOT NULL DEFAULT '-1' COMMENT 'BPM 审批状态',
`process_instance_id` varchar(64) DEFAULT NULL COMMENT '流程实例编号',
`remark` text COMMENT '备注',
`items` json DEFAULT NULL COMMENT '明细',
`file_urls` json DEFAULT NULL COMMENT '附件地址列表',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_no` (`tenant_id`, `no`),
KEY `idx_creator` (`tenant_id`, `creator`, `status`)
) ENGINE=InnoDB COMMENT='出差申请';
① no 由 OaNoRedisDAO 以 CC 为前缀生成,uk_no 保证租户内单号唯一。dept_id 关联 system_dept 表的 id 字段,创建时冗余当前用户所属部门。
② days 由起止时间经 LocalDateTimeUtils.getDaysBetweenCeiling 计算。status 是 BPM 审批状态,reimburse_status 是报销状态,两者用途不同:前者表示流程进度,后者表示是否已被差旅报销标记为已报销。
③ 枚举 reimburse_status 报销状态,对应字典 oa_reimburse_status:
| 值 | 说明 |
|---|---|
0 | 未报销 |
1 | 已报销 |
④ 行程明细保存在 items JSON 中,交通方式对应字典 oa_transport_type。结构如下:
items(行程明细)与 file_urls(附件)
items 对应 OaTravelApplyDO.Item:
[
{
"departureAreaId": 110101,
"arrivalAreaId": 310101,
"startTime": "2026-09-14T00:00:00",
"endTime": "2026-09-15T00:00:00",
"transportType": 2,
"remark": "高铁往返"
}
]
| 字段 | 类型 | 说明 |
|---|---|---|
departureAreaId | Long | 出发地区编号 |
arrivalAreaId | Long | 到达地区编号 |
startTime / endTime | LocalDateTime | 行程起止;明细内结束日期不能早于开始日期 |
transportType | Integer | 交通方式,字典 oa_transport_type:1 飞机 / 2 高铁/动车 / 3 火车 / 4 长途汽车 / 5 自驾 / 6 其他 |
remark | String | 备注 |
file_urls 是附件地址列表,形如 ["http://xxx/a.pdf"]。
# 1.2 状态流转
审批状态由 OaTravelApplyServiceImpl 与 BPM 共同控制,status 复用 BpmProcessInstanceStatusEnum,对应字典 bpm_process_instance_status(字典数据通常不含 -1):
| 状态值 | 枚举 | 说明 | 可执行操作 |
|---|---|---|---|
-1 | NOT_START | 未开始(草稿) | 编辑、删除、提交 |
1 | RUNNING | 审批中 | 撤回 |
2 | APPROVE | 审批通过 | —(已写出差考勤) |
3 | REJECT | 审批不通过 | 编辑、删除、重新提交 |
4 | CANCEL | 已取消 | 编辑、删除、重新提交 |
流程定义 Key 为 oa_travel_apply。与用品领用类似,草稿、驳回、取消均可再次编辑并提交。
状态流转说明
新增申请 ──→ 草稿(-1) ──提交──→ 审批中(1) ──┬──通过──→ 审批通过(2) → createApplyAttendance(TRAVEL)
↑ ├──不通过──→ 审批不通过(3) ─┐
│ └──撤回──→ 已取消(4) ───────┤
└──────────── 可再编辑并重新提交(-1 / 3 / 4)──────────────┘
- 创建(
createTravelApply):status = 未开始(-1),reimburseStatus = false,生成单号并冗余部门。 - 编辑 / 删除 / 提交(
updateTravelApply/deleteTravelApply/submitTravelApply):由validateTravelApplyEditable校验status ∈ {-1, 3, 4}。提交时写入审批中(1)并发起流程,流程变量含days。 - 撤回(
cancelTravelApply):仅审批中(1)可调用。 - 审批回调(
updateTravelApplyStatus,监听器OaTravelApplyStatusListener):审批通过(2)时调用attendanceService.createApplyAttendance(creator, TRAVEL, startTime),写入考勤类型出差;驳回、取消不写考勤。报销状态由差旅报销通过后另行回写。
# 1.3 管理后台
对应 [OA 办公协同 -> 出差管理 -> 出差申请] 菜单,对应 yudao-ui-admin-vue3 项目的 @/views/oa/travel/apply 目录。
# 列表

# 新增与修改
点击【新增】打开 OaTravelApplyForm.vue,填写事由、起止日期、同行人和预计费用,通过【添加行程】维护城市与交通方式,点击【保存】生成草稿。

# 提交与撤回
在列表点击【提交】,调用 POST /oa/travel-apply/submit 发起 oa_travel_apply 流程。审批中点击【撤回】,调用 PUT /oa/travel-apply/cancel。修改、删除和重新提交按单据当前状态校验。
# 查看报销情况
列表显示出差单审批状态和报销状态。查看详情时可核对行程、天数、预计费用和附件;是否可以关联差旅报销,由后端校验申请人及审批结果。
# 2. 差旅报销
差旅报销,由 OaTravelReimbursementController 提供接口(/oa/travel-reimbursement)。
# 2.1 表结构
省略 creator/create_time/updater/update_time/deleted/tenant_id 等通用字段
差旅报销表 oa_travel_reimbursement,保存差旅费用明细与汇总金额,可关联已通过的出差申请:
CREATE TABLE `oa_travel_reimbursement` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`no` varchar(64) NOT NULL COMMENT '单据编号',
`dept_id` bigint DEFAULT NULL COMMENT '申请部门编号',
`reason` text COMMENT '出差事由',
`start_time` datetime DEFAULT NULL COMMENT '开始日期',
`end_time` datetime DEFAULT NULL COMMENT '结束日期',
`days` int DEFAULT NULL COMMENT '出差天数',
`travel_apply_id` bigint DEFAULT NULL COMMENT '关联出差申请,可空',
`total_price` decimal(18,2) DEFAULT NULL COMMENT '报销总金额,元',
`pay_status` tinyint NOT NULL DEFAULT '0' COMMENT '支付状态',
`status` tinyint NOT NULL DEFAULT '-1' COMMENT 'BPM 审批状态',
`process_instance_id` varchar(64) DEFAULT NULL COMMENT '流程实例编号',
`remark` text COMMENT '备注',
`items` json DEFAULT NULL COMMENT '明细',
`file_urls` json DEFAULT NULL COMMENT '附件地址列表',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_no` (`tenant_id`, `no`),
KEY `idx_creator` (`tenant_id`, `creator`, `status`)
) ENGINE=InnoDB COMMENT='出差报销';
① no 以 CLBX 为前缀生成。travel_apply_id 可关联 oa_travel_apply 表的 id 字段,也允许不关联;关联时须本人且出差申请已审批通过,由 validateApprovedTravelApply 校验。
② total_price 由明细 items[].price 汇总(HALF_UP 两位小数)。pay_status 与审批状态分别保存:
| 值 | 字典 oa_pay_status |
|---|---|
0 | 未支付 |
1 | 已支付 |
③ 费用明细 items 对应字典 oa_expense_type。结构如下:
items(费用明细)
[
{
"expenseType": 1,
"expenseTime": "2026-09-14T08:00:00",
"departureCity": "上海",
"arrivalCity": "北京",
"price": 580.00,
"description": "往返高铁"
}
]
| 字段 | 类型 | 说明 |
|---|---|---|
expenseType | Integer | 费用类型:1 交通费 / 2 住宿费 / 3 餐饮费 / 4 通讯费 / 5 市内交通 / 6 其他 |
expenseTime | LocalDateTime | 费用发生时间 |
departureCity / arrivalCity | String | 出发地 / 到达地 |
price | BigDecimal | 金额,必填且 ≥ 0 |
description | String | 说明 |
附件 file_urls 必填(@NotEmpty)。
# 2.2 状态流转
差旅报销走独立 BPM 流程,Key 为 oa_travel_reimbursement。status 同样为 BpmProcessInstanceStatusEnum(-1/1/2/3/4)/ 字典 bpm_process_instance_status。编辑规则与出差申请相同:status ∈ {-1, 3, 4} 可改删提。
状态流转说明
新增报销 ──→ 草稿(-1) ──提交──→ 审批中(1) ──┬──通过──→ 审批通过(2)
↑ │ → 关联申请 reimburse_status=已报销
│ │ → pay_status 不自动改
│ ├──不通过──→ 审批不通过(3) ─┐
│ └──撤回──→ 已取消(4) ───────┤
└──────────── 可再编辑并重新提交(-1 / 3 / 4)──────────────┘
- 提交(
submitTravelReimbursement):再次校验关联出差单仍可用;发起流程,变量含days。 - 审批回调(
updateTravelReimbursementStatus,监听器OaTravelReimbursementStatusListener):状态与旧值相同则直接返回防重复;审批通过(2)且存在travel_apply_id时调用updateTravelApplyReimburseStatus,把申请标为已报销;不修改pay_status(仍保持未支付,支付需另行维护)。
# 2.3 管理后台
对应 [OA 办公协同 -> 出差管理 -> 差旅报销] 菜单,对应 yudao-ui-admin-vue3 项目的 @/views/oa/travel/reimbursement 目录。
# 列表

# 新增与修改
点击【新增】打开 OaTravelReimbursementForm.vue,按需关联出差单,填写出差事由和日期,点击【添加费用】逐行填写费用类型、发生日期、地点、金额及说明。总金额按明细汇总。

# 提交与撤回
保存草稿后,在列表点击【提交】,调用 POST /oa/travel-reimbursement/submit 发起 oa_travel_reimbursement 流程。审批中可点击【撤回】;后端在提交时再次校验关联出差单仍可使用。
# 查看详情
详情页在 @/views/oa/travel/reimbursement/detail/index.vue,展示关联出差单、费用明细、汇总金额、审批状态和支付状态。
# 3. 费用报销
费用报销,由 OaReimbursementController 提供接口(/oa/reimbursement)。
# 3.1 表结构
省略 creator/create_time/updater/update_time/deleted/tenant_id 等通用字段
费用报销表 oa_reimbursement,保存与出差无关的普通费用报销单及其费用明细:
CREATE TABLE `oa_reimbursement` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`title` varchar(255) NOT NULL COMMENT '申请标题',
`urgency` int NOT NULL COMMENT '紧急程度',
`reason` text NOT NULL COMMENT '申请原因',
`witness_user_id` bigint NOT NULL COMMENT '证明人用户编号',
`customer_name` varchar(255) DEFAULT NULL COMMENT '相关客户名称',
`payment_method` int NOT NULL COMMENT '报销方式',
`invoice_count` int NOT NULL COMMENT '票据总数',
`total_price` decimal(18,2) NOT NULL COMMENT '报销总金额,单位元',
`status` int NOT NULL COMMENT '审批状态',
`process_instance_id` varchar(255) DEFAULT NULL COMMENT 'BPM 流程实例编号',
`items` json NOT NULL COMMENT '报销明细',
`file_urls` json DEFAULT NULL COMMENT '附件地址列表',
PRIMARY KEY (`id`),
KEY `idx_tenant_creator` (`tenant_id`, `creator`, `deleted`),
KEY `idx_process_instance_id` (`process_instance_id`)
) ENGINE=InnoDB COMMENT='OA 费用报销申请';
① 没有业务单号 no,以主键 id 作为 BPM businessKey。witness_user_id 关联 system_users 表的 id 字段,创建/更新时由 adminUserApi.validateUser 校验;customer_name 只保存名称文本。
② 紧急程度对应字典 oa_apply_urgency(1 正常 / 2 重要 / 3 紧急);报销方式对应字典 oa_reimbursement_payment_method(1 银行卡 / 2 现金 / 3 其他)。
③ invoice_count、total_price 由后端按明细汇总。费用类型与差旅报销复用字典 oa_expense_type,但业务数据分别保存。本表不产生付款记录,也没有 pay_status 字段。
④ 明细结构如下:
items(报销明细)
[
{
"expenseTime": "2026-09-13T08:00:00",
"expenseType": 1,
"description": "客户项目交通",
"invoiceCount": 2,
"price": 12.34
}
]
| 字段 | 类型 | 说明 |
|---|---|---|
expenseTime | LocalDateTime | 费用发生时间 |
expenseType | Integer | 费用类型,字典 oa_expense_type |
description | String | 说明 |
invoiceCount | Integer | 本行票据数 |
price | BigDecimal | 本行金额 |
# 3.2 状态流转
流程定义 Key 为 oa_reimbursement。status 同样为 BpmProcessInstanceStatusEnum(-1/1/2/3/4)/ 字典 bpm_process_instance_status。
与出差申请、差旅报销不同:validateReimbursementEditable 仅允许草稿(-1)编辑和提交。驳回后不能再改(比用品领用、出差更严);无撤回、无删除接口。
| 状态值 | 枚举 | 说明 | 可执行操作 |
|---|---|---|---|
-1 | NOT_START | 未开始(草稿) | 编辑、提交 |
1 | RUNNING | 审批中 | — |
2 | APPROVE | 审批通过 | — |
3 | REJECT | 审批不通过 | —(不可再编辑) |
4 | CANCEL | 已取消 | — |
状态流转说明
新增报销 ──→ 草稿(-1) ──提交──→ 审批中(1) ──┬──通过──→ 审批通过(2)
├──不通过──→ 审批不通过(3)(终态,不可再改)
└──取消──→ 已取消(4)
- 创建(
createReimbursement):显式设为未开始(-1),汇总票据数与总金额。 - 编辑 / 提交(
updateReimbursement/submitReimbursement):仅草稿可操作;提交后status = 审批中(1),流程变量为totalPrice。 - 审批回调(
updateReimbursementStatus,监听器OaReimbursementStatusListener):只回写status,无支付、记账或生成凭证等副作用。
# 3.3 管理后台
对应 [OA 办公协同 -> 流程管理 -> 费用报销] 菜单,对应 yudao-ui-admin-vue3 项目的 @/views/oa/reimbursement 目录。
# 列表

# 新增与修改
点击【新增】打开 OaReimbursementForm.vue,填写标题、紧急程度、证明人、客户名称、报销方式和原因,逐行填写费用发生时间、类型、说明、票据数及金额,上传附件后保存。

# 提交与进度
在列表点击【提交】,调用 POST /oa/reimbursement/submit 发起 oa_reimbursement 流程。通过流程进度查看审批,业务详情在 @/views/oa/reimbursement/detail/index.vue。详情可由申请人或实际流程参与人访问,不能仅凭单据编号查看他人申请。
# 审批结果
审批结束后回写 status。本申请用于费用审批,不在此执行支付、记账或生成 FMS 凭证。