差旅报销管理真相:90%的OA系统都必须考虑——出差申请+差旅报销双单联动全流程设计拆解
🌐 文档地址:ruoyioffice.com | 📦 源码1:ruoyi-office-vben |📦 源码2:ruoyi-office |📦 源码3:ruoyi-office | 💬 :17156169080(备注「RuoYi Office」)
一家200人的制造企业,每月200+笔出差申请、150+笔差旅报销。Excel报销单在聊天软件群里转了三圈还没到财务手里,出差标准靠口头传达,同一张机票被报销两次直到年底审计才发现——这不是段子,这是中国中小企业差旅管理的日常。 差旅管理看似只是"填个表、贴个票",背后却是出差申请→审批→出行→报销→核验→支付的完整业务闭环。做好了,是降本增效的利器;做不好,就是财务黑洞。
引言:差旅管理到底难在哪?
如果你问一个中小企业的财务主管"最头疼的事是什么",大概率会得到两个答案:工资和报销。而差旅报销,又是报销中最复杂的那个——因为它不只是一张报销单的事。
一次完整的差旅业务涉及多个阶段和多个角色:
- 📝 出差前:员工申请出差,填写行程计划,领导审批是否同意
- ✈️ 出差中:按行程出行,产生交通、住宿、餐饮等各类费用
- 💰 出差后:整理票据、填报费用明细、关联出差申请、提交报销审批
- ✅ 审批后:部门确认→财务核验票据→打款→标记已支付
这些环节在很多中小企业里是割裂的:
| 环节 | 现状痛点 | 后果 |
|---|---|---|
| 出差申请 | 口头请示或聊天软件报备,无正式记录 | 事后扯皮——"我不知道你出差了" |
| 行程管理 | 没有行程明细,出差目的和路线不透明 | 合规审计无法追溯 |
| 费用报销 | Excel表格+纸质贴票,流转靠手递 | 效率低,周期长达2-4周 |
| 票据核验 | 财务人工逐张比对发票和报销金额 | 重复报销、假发票难以发现 |
| 出差↔报销关联 | 两者独立,无系统联动 | 同一次出差被多次报销,或出差了忘记报销 |
RuoYi Office 的差旅管理模块,就是为解决这些痛点而设计的。 它把出差申请和差旅报销作为一个业务闭环来设计——出差申请审批通过是报销的前提,报销审批通过自动回写出差单状态——用两张主表+两张子表、两套BPM流程、一个联动机制,覆盖了中小企业差旅管理80%的核心需求。
一、业务设计:出差申请与差旅报销的关系
1.1 两单联动的核心设计理念
差旅管理不是两个独立功能的简单堆砌,而是一个闭环业务。RuoYi Office 的核心设计理念是:
出差申请(审批通过)
│
├─ 报销单创建时 → 选择出差申请 → 自动填充事由/日期/天数
│
└─ 报销单审批通过 → 回写出差申请 reimburse_status = 已报销
设计原则:
- 出差申请是报销的"源头凭证":报销时可关联已审批通过的出差申请,系统自动带入出差事由、日期、天数等信息,减少重复填写
- 报销状态反写出差单:报销审批通过后,自动更新对应出差申请的报销状态,防止同一次出差被多次报销
- 允许独立报销:关联出差申请不是强制的——临时出差或补报场景下,可以不关联出差单直接报销
这种"有关联但不强制"的设计,既保证了正常流程的数据完整性,又兼顾了中小企业的灵活性需求。过度严格的系统会被员工绕过,反而不如一套"80%规范+20%灵活"的方案有效。
1.2 出差申请单功能概览
员工发起出差申请,填写出差事由、行程明细(支持多段行程),审批通过后可据此发起差旅报销。
▲ 出差申请列表页:展示单据编号、单据状态、出差事由、日期范围、天数、预计费用、报销状态等核心信息
列表页的设计亮点:
- 报销状态列:直观展示每条出差申请是"未报销"还是"已报销",财务一目了然
- 单据编号链接:点击编号直接跳转详情页查看完整信息
- 字典渲染:单据状态、报销状态均通过字典标签渲染,颜色区分状态
1.3 差旅报销单功能概览
出差完成后,员工发起差旅报销,可选择关联已审批通过的出差申请单(系统自动带入行程信息),填写实际费用明细(交通、住宿、餐饮、其他),上传票据附件,提交审批。

▲ 差旅报销列表页:展示单据编号、关联出差单号、出差事由、日期范围、报销总金额、支付状态
二、数据模型设计:4张表搞定差旅闭环
2.1 ER 关系总览
┌──────────────────────────────────┐
│ oa_business_trip │
│ (出差申请单 - 主表) │
│ ├ bill_code (单据编号) │
│ ├ trip_reason (出差事由) │
│ ├ start_date / end_date │
│ ├ total_days (自动计算) │
│ ├ companion (同行人) │
│ ├ estimated_cost (预计费用) │
│ ├ reimburse_status (报销状态) │ ← 报销审批通过后自动回写
│ └ process_instance_id │
└──────────────┬───────────────────┘
│ 1 : N
▼
┌──────────────────────────────────┐
│ oa_business_trip_item │
│ (行程明细 - 子表) │
│ ├ trip_id → 出差申请单 │
│ ├ departure_city (出发城市) │
│ ├ arrival_city (到达城市) │
│ ├ start_date / end_date │
│ ├ transport_type (交通方式) │
│ └ remark (备注) │
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ oa_travel_reimburse │
│ (差旅报销单 - 主表) │
│ ├ bill_code (单据编号) │
│ ├ trip_id → 关联出差申请单 │ ← 联动核心
│ ├ trip_bill_code (冗余) │
│ ├ trip_reason / start_date ... │
│ ├ total_amount (自动汇总) │
│ ├ pay_status (支付状态-预留) │
│ └ process_instance_id │
└──────────────┬───────────────────┘
│ 1 : N
▼
┌──────────────────────────────────┐
│ oa_travel_reimburse_item │
│ (费用明细 - 子表) │
│ ├ reimburse_id → 报销单 │
│ ├ expense_type (费用类型) │
│ ├ expense_date (发生日期) │
│ ├ departure_city / arrival_city │
│ ├ amount (金额) │
│ ├ description (费用说明) │
│ ├ invoice_code (发票代码-预留) │
│ ├ invoice_no (发票号码-预留) │
│ └ invoice_amount (发票金额-预留)│
└──────────────────────────────────┘
2.2 为什么用主子表而不是单表?
这是一个关键的设计决策。很多简易差旅系统把行程信息直接塞在出差申请主表里(比如用逗号分隔的字段存出发地和目的地),报销也只有一个总金额字段。这种设计在真实业务中根本不够用:
| 设计方案 | 出差"上海→北京→广州" 三段行程 | 报销"交通+住宿+餐饮" 三类费用 |
|---|---|---|
| 单表方案 | 出发城市=上海,北京 / 到达城市=北京,广州(逗号拼接,无法独立管理每段行程) | 总金额=5000(无法看到费用构成) |
| 主子表方案 | 三条行程记录,每段有独立的日期、交通方式、备注 | 三条费用记录,每笔有类型、金额、说明 |
主子表的核心价值:
- 数据粒度精确:每段行程、每笔费用都是独立记录,便于统计分析(如"全公司交通费占比多少")
- 前端交互友好:VxeGrid 表格支持增删行,操作直观,不需要解析逗号分隔字符串
- 发票预留:费用明细子表预留了
invoice_code/invoice_no/invoice_amount字段,为未来的 AI 发票识别功能做准备 - 审计追溯:每笔费用都有明确的发生日期和说明,满足财务审计要求
2.3 核心字段设计解读
出差申请单核心字段:
| 字段 | 类型 | 设计考量 |
|---|---|---|
trip_reason | varchar(500) | 出差事由,同时作为流程变量 cause 传入 BPM |
total_days | decimal(5,1) | 自动计算(结束日期 - 开始日期 + 1),支持 0.5 天精度 |
estimated_cost | decimal(12,2) | 可选填,作为流程变量用于条件分支(超过阈值加审批) |
reimburse_status | tinyint | 0-未报销 1-部分报销 2-已报销,报销审批通过后自动更新 |
companion | varchar(200) | 同行人,便于行政统计出差频次 |
差旅报销单核心字段:
| 字段 | 类型 | 设计考量 |
|---|---|---|
trip_id | bigint | 可为空——允许不关联出差申请直接报销 |
trip_bill_code | varchar(32) | 冗余存储出差单编号,列表页可直接展示,避免关联查询 |
total_amount | decimal(12,2) | 自动从费用明细汇总,作为流程变量用于审批条件分支 |
pay_status | tinyint | 预留字段,审批通过后默认 0-未支付,后续对接支付模块 |
设计哲学:字段设计遵循"本期够用+远期可扩展"原则——核心业务字段完整实现,发票识别和支付对接通过预留字段为未来迭代做准备。
三、审批流程设计:精简但不简陋
3.1 出差申请审批流程
面向中小企业的精简审批流程,核心原则是"小额快审、大额严审":
申请人提交 → 部门经理审批 ──→ 审批通过
│
└──→ 出差天数 > 3天 或 预计费用 > 5000元
→ 分管领导审批 → 审批通过
| 节点 | 审批人 | 核心操作 | 条件逻辑 |
|---|---|---|---|
| 申请人提交 | 发起人(自动) | 填写出差申请单并提交 | — |
| 部门经理审批 | 发起人直属上级 | 审核出差事由和行程合理性 | 天数 ≤ 3 且 费用 ≤ 5000 → 直接通过 |
| 分管领导审批 | 上级部门负责人 | 审批长期或高额出差 | 天数 > 3 或 费用 > 5000 时触发 |
流程变量传递(代码实现):
Map<String, Object> processInstanceVariables =
BpmProcessVariableUtils.buildBillVariables(saveReqVO);
processInstanceVariables.put("totalDays", saveReqVO.getTotalDays());
processInstanceVariables.put("estimatedCost", saveReqVO.getEstimatedCost());
天数和费用作为流程变量传入 Flowable,可在流程设计器中直接配置条件表达式
${totalDays > 3 || estimatedCost > 5000},阈值可视化配置、零代码调整。
3.2 差旅报销审批流程
报销审批比出差申请多一个财务审核节点,确保费用合规:
申请人提交 → 部门经理审批 ──→ 财务审核 → 审批通过
│
└──→ 报销金额 > 5000元
→ 分管领导审批 → 财务审核 → 审批通过
| 节点 | 审批人 | 审核重点 |
|---|---|---|
| 部门经理审批 | 直属上级 | 出差事由真实性、费用合理性 |
| 分管领导审批 | 分管领导 | 大额费用的必要性(条件触发) |
| 财务审核 | 财务部指定人员 | 发票合规性、金额准确性、报销标准 |
为什么报销多了财务审核? 因为出差申请是"意向审批"(同不同意你去出差),而报销是"实际支出审批"(这些钱该不该付)。前者只需要业务领导判断,后者必须有财务专业把关。
3.3 审批通过后的业务联动
差旅报销最核心的联动逻辑发生在审批通过的瞬间——系统自动回写出差申请的报销状态:
@Override
@Transactional(rollbackFor = Exception.class)
public void updateProcessStatus(String businessKey, Integer status) {
Long id = Long.parseLong(businessKey);
TravelReimburseDO bill = travelReimburseMapper.selectById(id);
TravelReimburseDO updateObj = new TravelReimburseDO();
updateObj.setId(id);
updateObj.setProcessStatus(status);
if (APPROVE.getStatus().equals(status)) {
updateObj.setPayStatus(0); // 审批通过 → 标记为未支付
if (bill.getTripId() != null) {
// 核心联动:回写出差申请单的报销状态
BusinessTripDO tripUpdate = new BusinessTripDO();
tripUpdate.setId(bill.getTripId());
tripUpdate.setReimburseStatus(2); // 已报销
businessTripMapper.updateById(tripUpdate);
}
}
travelReimburseMapper.updateById(updateObj);
}
这段代码的精妙之处:
- 事务保证:
@Transactional确保报销状态更新和出差单回写在同一个事务中,要么都成功要么都回滚 - 空值安全:
bill.getTripId() != null判断——如果报销单没有关联出差申请,就不执行回写 - 支付预留:审批通过同时设置
payStatus = 0(未支付),为后续对接支付模块预留入口 - FlowBillService 回调:这是 RuoYi Office BPM 架构的标准回调方法,由 Flowable 引擎在流程状态变更时自动触发
四、前端实现:主子表交互的最佳实践
4.1 出差申请表单:BasicForm + 行程明细子表

▲ 出差申请表单详情:单据头(编号/申请人/日期/单位/部门)+ 基本信息 + 行程明细 + 附件
出差申请表单的核心交互设计:
1. 天数自动计算
const calculatedDays = computed(() => {
const startDate = formData.value.startDate;
const endDate = formData.value.endDate;
if (startDate && endDate) {
const diff = dayjs(endDate).diff(dayjs(startDate), 'day') + 1;
return diff > 0 ? diff : undefined;
}
return undefined;
});
选择开始/结束日期后,天数字段自动计算并填充,无需手动输入。"+1"的设计符合出差天数的业务含义(含首尾两天)。
2. 行程明细 VxeGrid 子表
行程明细采用 VxeGrid 可编辑表格实现,每行包含出发城市、到达城市、日期范围、交通方式:
function handleAddItem() {
const newItem: BusinessTripApi.BusinessTripItem = {
departureCity: '',
arrivalCity: '',
startDate: formData.value.startDate, // 自动继承主表开始日期
endDate: formData.value.endDate, // 自动继承主表结束日期
transportType: undefined,
remark: '',
};
itemList.value.push(newItem);
gridApi.grid.reloadData(itemList.value);
}
交互细节:新增行程时,默认继承主表的开始/结束日期,减少重复输入。交通方式通过字典驱动(飞机/高铁/火车/汽车/自驾/其他),下拉选择。
3. 编辑/只读双模态
表单通过 readonly 变量控制所有字段和子表的编辑状态。在审批中/审批通过状态下,表单自动切换为只读模式:
<Input
v-if="!readonly"
v-model:value="row.departureCity"
placeholder="出发城市"
size="small"
/>
<span v-else>{{ row.departureCity }}</span>
每个子表单元格都有两种渲染模式——可编辑时显示输入组件,只读时显示纯文本。这种设计比简单的 disabled 属性体验更好(disabled 输入框样式灰暗,阅读体验差)。
4.2 差旅报销表单:联动选择 + 费用汇总
差旅报销表单是整个差旅模块最复杂的前端页面,核心亮点是出差申请选择联动和费用自动汇总。

1. 出差申请选择弹窗
点击"关联出差申请"字段旁的选择按钮,弹出出差申请选择弹窗:
// 查询当前用户已审批通过 + 未完全报销的出差申请
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const fullList = await getApprovedBusinessTripList();
const filteredList = filterTrips(fullList, formValues);
return {
list: filteredList.slice(start, end),
total: filteredList.length,
};
},
},
},
radioConfig: { labelField: 'id', trigger: 'row' }, // 单选模式
},
gridEvents: {
cellDblclick: ({ row }) => {
state.selectedTrip = row;
handleConfirm(); // 双击快速选择
},
},
});
弹窗支持搜索筛选(按单据编号、出差事由、报销状态),单选确认或双击快速选择。
2. 选择后自动填充
选择出差申请后,系统自动将出差单的事由、日期、天数等信息填充到报销单:
function handleTripSelect(trip: BusinessTripApi.BusinessTrip) {
formData.value.tripId = trip.id;
formData.value.tripBillCode = trip.billCode;
formData.value.tripReason = trip.tripReason;
formData.value.startDate = trip.startDate;
formData.value.endDate = trip.endDate;
formData.value.totalDays = trip.totalDays;
if (basicFormRef.value) {
basicFormRef.value.setFormValues({ ... }, false);
basicFormRef.value.clearFieldError('tripBillCode');
}
}
这个"选择即填充"的设计大幅减少了员工的填写工作量——出差事由、日期这些信息在出差申请时已经填过一遍了,报销时不应该再手动输入。
3. 费用明细自动汇总
报销总金额不需要手动输入,而是从费用明细子表实时汇总计算:
const totalAmount = computed(() => {
return itemList.value.reduce((sum, item) => {
return sum + (Number(item.amount) || 0);
}, 0);
});
VxeGrid 表格底部还有合计行实时展示:
footerMethod: () => {
return [['合计', '', '', '', `¥ ${totalAmount.value.toFixed(2)}`, '', '']];
},
4. 报销规范提示
表单底部的报销规范提示是一个贴心的设计——用 Ant Design Vue 的 Alert 组件展示 8 条报销规范,确保员工提交前知晓要求:
const reimburseNoticeContent = [
'每笔费用须附对应发票或收据,附件中请备注发票金额',
'发票抬头必须为公司全称,且与报销主体一致',
'发票日期须在出差期间范围内(前后不超过 3 个工作日)',
'交通费用须提供车票、机票行程单或电子客票',
'住宿费须提供住宿发票,发票须注明入住日期和天数',
'餐饮费每日限额以公司差旅标准为准',
'同一行程不可重复报销,系统会自动检查关联出差单的报销状态',
'报销金额须与发票金额一致,如有差异请在费用说明中注明原因',
];

为什么把规范写在表单里而不是弄个单独的公告? 因为"离场景最近的提示才是有效的提示"。员工在填报销单的时候看到规范,比在公告栏里发一条"关于差旅报销规范的通知"有效100倍。
5. 提交前校验
差旅报销提交前有三重校验,确保数据完整性:
if (itemList.value.length === 0) {
message.warning('请添加费用明细');
return;
}
const invalidAmountIndex = itemList.value.findIndex(
(item) => item.amount === undefined || item.amount === null,
);
if (invalidAmountIndex !== -1) {
message.warning(`第${invalidAmountIndex + 1}行费用明细金额不能为空`);
return;
}
if (!formData.value.attachments?.length) {
message.warning('请上传附件');
return;
}
三重校验:① 费用明细不能为空;② 每行金额不能为空(精确到第几行);③ 必须上传附件(发票/收据)。后端也有对应的 validateSubmitData() 方法做二次校验。
五、后端架构:FlowBillService 标准范式
5.1 出差申请 Service:标准的 BPM 业务模块
出差申请的后端实现完美展示了 RuoYi Office 的业务模块接入 BPM 的标准范式:
@Slf4j
@Service
@Validated
public class BusinessTripServiceImpl
implements BusinessTripService, FlowBillService<OaBillTypeEnum> {
@Override
@Transactional(rollbackFor = Exception.class)
public Long submitBusinessTrip(BusinessTripSaveReqVO saveReqVO) {
// 1. 自动生成单据编号
if (StringUtils.isBlank(saveReqVO.getBillCode())) {
saveReqVO.setBillCode(BillCodeUtils.generateBillCode(
SystemEnum.OA, OaBillTypeEnum.OA_BUSINESS_TRIP));
}
// 2. 保存主表 + 子表
BusinessTripDO bill = BeanUtils.toBean(saveReqVO, BusinessTripDO.class)
.setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus());
businessTripMapper.insertOrUpdate(bill);
saveItems(bill.getId(), saveReqVO.getItems());
// 3. 构建流程变量(天数、费用用于条件分支)
Map<String, Object> processInstanceVariables =
BpmProcessVariableUtils.buildBillVariables(saveReqVO);
processInstanceVariables.put("totalDays", saveReqVO.getTotalDays());
processInstanceVariables.put("estimatedCost", saveReqVO.getEstimatedCost());
// 4. 发起流程实例
String processInstanceId = processInstanceApi.submitProcessInstance(
Long.valueOf(saveReqVO.getCreator()),
new BpmProcessInstanceCreateReqDTO()
.setProcessDefinitionKey(
OaBillTypeEnum.OA_BUSINESS_TRIP.getProcessDefinitionKey())
.setVariables(processInstanceVariables)
.setBusinessKey(String.valueOf(bill.getId()))
).getCheckedData();
// 5. 回写流程实例ID + 保存附件
businessTripMapper.updateById(
new BusinessTripDO().setId(bill.getId())
.setProcessInstanceId(processInstanceId));
if (saveReqVO.getAttachments() != null) {
attachmentService.saveAttachmentList(
OaBillTypeEnum.OA_BUSINESS_TRIP.getTypeCode(),
bill.getId(), saveReqVO.getAttachments());
}
return bill.getId();
}
// FlowBillService 接口实现
@Override
public OaBillTypeEnum getSupportedBillType() {
return OaBillTypeEnum.OA_BUSINESS_TRIP;
}
@Override
public void updateProcessStatus(String businessKey, Integer status) {
Long id = Long.parseLong(businessKey);
businessTripMapper.updateById(
new BusinessTripDO().setId(id).setProcessStatus(status));
}
}
这个 Service 的代码结构就是 RuoYi Office 所有 BPM 业务模块的模板——新增一个业务单据(合同审批、采购申请等),照着这个结构实现就行。核心要点:
implements FlowBillService<OaBillTypeEnum>:声明本模块支持的单据类型,框架自动注册到工厂insertOrUpdate():支持"暂存再提交"的两步操作BpmProcessVariableUtils.buildBillVariables():自动提取标准流程变量- 自定义流程变量:
totalDays/estimatedCost作为条件分支的判断依据 - 附件独立管理:通过
AttachmentService统一管理,解耦业务逻辑
5.2 子表操作:先删后增的简洁策略
行程明细和费用明细的保存都采用**"先删后增"**策略:
private void saveItems(Long billId, List<BusinessTripItemVO> items) {
businessTripItemMapper.deleteByTripId(billId);
if (items != null && !items.isEmpty()) {
List<BusinessTripItemDO> itemDOs = BeanUtils.toBean(items, BusinessTripItemDO.class);
for (BusinessTripItemDO itemDO : itemDOs) {
itemDO.setId(null); // 清空ID,确保新增而非更新
itemDO.setTripId(billId);
}
businessTripItemMapper.insertBatch(itemDOs);
}
}
为什么用"先删后增"而不是逐条对比更新? 因为前端的子表操作场景是:用户可能增加、删除、修改任意行。逐条对比的代码复杂度是 O(n²)(新旧列表交叉比对),而先删后增只需 O(n),代码简洁且不容易出bug。对于子表通常只有几条到十几条记录的场景,性能完全可以接受。
5.3 报销审批校验:后端兜底
private void validateSubmitData(TravelReimburseSaveReqVO saveReqVO) {
if (CollectionUtils.isEmpty(saveReqVO.getItems())) {
throw invalidParamException("请添加费用明细");
}
for (int i = 0; i < saveReqVO.getItems().size(); i++) {
TravelReimburseItemVO item = saveReqVO.getItems().get(i);
if (item.getAmount() == null) {
throw invalidParamException("第{}行费用明细金额不能为空", i + 1);
}
}
if (CollectionUtils.isEmpty(saveReqVO.getAttachments())) {
throw invalidParamException("请上传附件");
}
}
前端校验可以被绕过(浏览器开发者工具),后端校验才是数据完整性的最后防线。错误信息精确到"第N行",帮助用户快速定位问题。
六、字典数据:零代码扩展的枚举管理
差旅模块涉及的所有枚举值均通过字典服务管理:
| 字典类型 | 字典值 | 使用场景 |
|---|---|---|
oa_transport_type | 飞机、高铁/动车、火车、长途汽车、自驾、其他 | 行程明细交通方式选择 |
oa_expense_type | 交通费、住宿费、餐饮费、通讯费、市内交通、其他 | 费用明细类型选择 |
oa_reimburse_status | 未报销、部分报销、已报销 | 出差申请单报销状态 |
oa_pay_status | 未支付、已支付 | 报销单支付状态(预留) |
前端通过 getDictOptions(DICT_TYPE.OA_TRANSPORT_TYPE, 'number') 一行代码获取选项列表,管理员在后台添加新交通方式(如"网约车")无需改代码。
七、API 接口设计
出差申请 API
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /oa/business-trip/save | 保存草稿(可反复暂存) |
| POST | /oa/business-trip/submit | 提交审批(触发流程) |
| GET | /oa/business-trip/get?id=xxx | 获取详情(含行程明细+附件) |
| GET | /oa/business-trip/page | 分页列表 |
| DELETE | /oa/business-trip/delete?id=xxx | 删除(含附件+子表+流程实例清理) |
| GET | /oa/business-trip/get-approved-list | 获取已审批出差单(供报销选择弹窗) |
差旅报销 API
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /oa/travel-reimburse/save | 保存草稿 |
| POST | /oa/travel-reimburse/submit | 提交审批(含数据校验) |
| GET | /oa/travel-reimburse/get?id=xxx | 获取详情(含费用明细+附件) |
| GET | /oa/travel-reimburse/page | 分页列表 |
| DELETE | /oa/travel-reimburse/delete?id=xxx | 删除 |
get-approved-list是为联动设计的专属接口——只返回当前用户已审批通过且未完全报销的出差申请,供报销单选择弹窗使用。
八、前端目录结构
views/oa/trip/
├── tripapply/ # 出差申请
│ ├── list/
│ │ ├── index.vue # 列表页(VxeGrid + 搜索表单)
│ │ └── data.ts # 列定义 + 搜索表单 schema
│ └── info/
│ ├── index.vue # 表单页(BasicForm + 行程明细 + 附件)
│ └── data.ts # 表单 schema + 交通方式字典
├── tripreimburse/ # 差旅报销
│ ├── list/
│ │ ├── index.vue # 列表页
│ │ └── data.ts # 列定义
│ └── info/
│ ├── index.vue # 表单页(BasicForm + 费用明细 + 附件 + 报销规范)
│ └── data.ts # 表单 schema + 费用类型字典
└── components/
├── trip-select-modal.vue # 出差申请选择弹窗(报销关联用)
├── trip-select-data.ts # 弹窗列定义 + 搜索 schema
└── index.ts # 统一导出
api/oa/trip/
├── tripapply/index.ts # 出差申请 API + TypeScript 类型
└── tripreimburse/index.ts # 差旅报销 API + TypeScript 类型
这套目录结构完全遵循 RuoYi Office 的前端开发规范:API 层和页面层按模块同构组织,每个功能模块的 list/(列表)和 info/(表单)独立分包,组件抽离到 components/。
九、事件驱动全景:BPM 与业务的完美协作
┌────────────────────────────────┐
│ BusinessTripServiceImpl │
│ ├ getSupportedBillType() │
│ │ → OA_BUSINESS_TRIP │
Flowable 引擎 │ ├ updateProcessStatus() │
↓ │ │ → 更新 processStatus │
BpmProcessInstanceEventListener │ └ (无 onProcessApproved) │
↓ └────────────────────────────────┘
Spring ApplicationEvent ─────────→
↓ ┌────────────────────────────────┐
OaLocalNotificationListener │ TravelReimburseServiceImpl │
↓ │ ├ getSupportedBillType() │
OaFlowBillServiceFactory │ │ → OA_TRAVEL_REIMBURSE │
↓ │ ├ updateProcessStatus() │
根据 processDefinitionKey │ │ → 更新 processStatus │
匹配对应 Service │ │ → 审批通过时回写出差单 │
│ │ reimburseStatus = 已报销│
│ └ payStatus = 0 (未支付) │
└────────────────────────────────┘
仅需实现 FlowBillService 接口,出差申请和差旅报销就完成了与 BPM 引擎的全部集成。
十、远期规划:AI赋能差旅管理
当前版本聚焦核心 80% 需求,但数据模型和架构设计已经为未来迭代做好了准备:
10.1 发票智能识别(AI多模态)
项目已集成 Spring AI 1.1.2 + 多模态大模型(通义千问 qwen-vl / 智谱 glm-4v),未来可实现:
上传发票图片 → AI识别发票信息 → 自动填充 invoice_code/invoice_no/invoice_amount
→ 与报销明细金额比对 → 不一致时弹出警告
费用明细子表已预留 invoice_code、invoice_no、invoice_amount 三个字段,开箱即可对接。
10.2 财务支付对接
报销审批通过 → 生成 PayOrder(支付单)→ 财务确认打款
→ 更新 pay_status = 已支付 → 同步生成记账凭证
报销单的 pay_status(支付状态)和 pay_time(支付时间)字段已预留,后续对接 yudao-module-pay 即可打通。
10.3 出差标准管控
按职级/部门配置差旅标准(如"总监级住宿上限 800元/晚"),提交报销时自动比对:
费用明细金额 vs 差旅标准限额 → 超标提示 / 强制拒绝 / 审批加签
十一、总结:差旅模块的设计亮点
| 能力 | 实现方式 | 价值 |
|---|---|---|
| 双单联动 | 报销单关联出差单,自动填充+状态回写 | 减少填写、防止重复报销 |
| 主子表建模 | 4张表:行程明细+费用明细独立管理 | 数据粒度精确,利于统计分析 |
| 智能条件审批 | 流程变量驱动条件分支 | 小额快审、大额严审,零代码调整阈值 |
| 费用自动汇总 | computed 实时计算 + VxeGrid 合计行 | 减少人工计算错误 |
| 报销规范内置 | Alert 组件嵌入表单 | 离场景最近的提示 |
| 前后端双校验 | 前端三重校验 + 后端兜底校验 | 数据完整性有保障 |
| 字典驱动 | 交通方式/费用类型/报销状态均字典管理 | 管理员可自行扩展 |
| 事件驱动解耦 | FlowBillService + Spring Event | 业务模块与 BPM 零耦合 |
| 远期可扩展 | 预留发票/支付字段 + Spring AI 集成 | AI 发票识别、自动支付开箱即用 |
| 多租户隔离 | 全表带 tenant_id | SaaS 场景开箱即用 |
十二、快速体验
在线体验
- 演示地址:ruoyioffice.com/web/
- 账号密码:admin / admin123
- 操作路径:OA 协同办公 → 出差管理 → 出差申请 / 差旅报销
源码获取
| 平台 | 仓库地址 |
|---|---|
| GitCode(后端) | gitcode.com/zhouzhongya… |
| GitCode(前端) | gitcode.com/zhouzhongya… |
| GitHub(后端) | [github.com/yuqing2026/…github.com/yuqing2026/… |