编辑 | blame | 历史 | 原始文档

财务-发票报销 - 前端联调方案

模块:yudao-module-finance(独立模块,不与其他业务模块关联)
后端接口前缀:/finance/reimbursement
附件关联:recordType = finance_reimbursement,application = file

涉及页面

前端 finance 目录已按本项目约定建好,共 8 个文件(含字典枚举注册):

文件 说明
src/packages/constants/src/dict-enum.ts 新增 FINANCE_DICT(FINANCE_REIMBURSEMENT_STATUS / FINANCE_EXPENSE_TYPE),并加入 DICT_TYPE 汇总导出
src/api/finance/reimbursement/index.ts 报销单 API 封装
src/views/finance/reimbursement/index.vue 报销单列表页(菜单指向此组件)
src/views/finance/reimbursement/data.ts 表单排布 + 表格列定义 + 状态常量 ReimbursementStatus
src/views/finance/reimbursement/modules/form.vue 新建/编辑/详情弹窗(主表字段 + 发票上传)
src/views/finance/reimbursement/modules/item-form.vue 报销明细行编辑(可增删行,金额汇总回填主表)
src/views/finance/reimbursement/modules/process-select-modal.vue 选择审批流程弹窗
src/views/finance/reimbursement/detail/approve/index.vue 审批详情页(BPM 审批中心动态加载用)

字典枚举必须登记到 dict-enum.ts,否则 DICT_TYPE.FINANCE_* 取不到值,列表与表单下拉会空白。

菜单已配置完成,无需前端手工加路由(本项目为动态路由):

菜单 id 路径 组件 说明
财务报销 1075425 /finance — 顶级目录(parent_id=0,sort=75,排在「售后管理」与「基础设施」之间)
报销单 1075417 reimbursement finance/reimbursement/index 挂在「财务报销」下

报销单为**独立模块**,因此刻意**不挂在 ERP 系统 → 财务管理**下(那里是付款单/收款单/结算账户,属 ERP 业务),而是新建顶级目录「财务报销」自成一条链路。

⚠ 菜单位置易错点:报销单是**顶级菜单的二级项** —— 侧边栏展开 「财务报销」 即可看到;面包屑显示为 财务报销 > 报销单,最终路由 /finance/reimbursement(**注意不是** /erp/finance/reimbursement)。ERP 系统下的「财务管理」里**没有**报销单。

⚠ 授权易错点:system_role_menu 中 1075425 与 1075417 均未授予任何角色,因此**只有超级管理员**(super_admin,代码直返全部菜单)能看到;普通角色登录看不到,需要在「系统管理 → 角色」里勾选授权。

菜单**每次进入应用都会重新拉取**(accessStore 的 persist.pick 只含 token/accessCodes,不含 accessMenus),所以不存在「需要清缓存才看得到」的问题;看不到只可能是**位置没展开**或**账号角色未授权**。

字典已配置完成,前端直接用 getDictOptions / DictTag 取值,无需自建枚举:

字典类型 名称 取值
finance_reimbursement_status 报销状态 0 草稿 / 15 审批中 / 20 已通过 / 35 已驳回 / 40 已归档
finance_expense_type 费用类型 1 差旅 / 2 办公 / 3 招待 / 4 交通 / 5 其他

按钮权限标识(已落 system_menu,前端用 v-access:code 或 AccessControl 控制显隐):

finance:reimbursement:query、:create、:update、:delete、:export、:update-status、:archive

业务流程与数据带入

新建报销单(填主表 + 明细行 + 上传发票)
        │
        ├── 主表字段:报销标题、报销总金额、申请日期、备注
        └── 明细行(1..n):费用类型、发票号码、发票金额、发票日期、开票方、费用说明
                    │
                保存(create)
                    │  ← 附件:先上传拿 blobId,再把全部 blobId 放进本次请求体一起提交
                    ▼
              status = 0 草稿
                    │
              【提交审批】选流程 → 生成 BPM 实例
                    ▼
              status = 15 审批中  ←──────────────┐
                    │                            │
         ┌──────────┴──────────┐                 │
    审批通过                 审批驳回              │
         ▼                      ▼                │
   status = 20 已通过      status = 35 已驳回 ──┘ 重新提交
         │
    【归档】
         ▼
   status = 40 已归档(终态)

数据带入要点

  1. 明细行不单独调用接口:明细随主表的 create / update 一次性提交(items 数组),后端按「先删后增」全量覆盖。前端不需要逐行调接口。
  2. 发票附件两步走(不得在业务表存文件地址):
  3. 先调 /system/storage-blob/upload 拿到 blobId;
  4. 保存报销单时把**该单当前全部** blobId 放进 blobIds 一起提交,由后端统一绑定到 recordType=finance_reimbursement + recordId=报销单id。
  5. 主表「报销总金额」建议前端自动汇总:后端不校验 totalAmount 与 items[].invoiceAmount 之和是否相等,直接入库。若要保证一致,请前端在 items 变化时把 invoiceAmount 求和回填 totalAmount(用户仍可手改)。
  6. 审批流程 key 来自 approve-process-list:前端不得写死流程 key,必须实时取列表后由用户选择(后端在提交时会按所选 key 创建流程实例)。
  7. 审批详情页跳转:BPM 审批中心带 processInstanceId 跳转,前端应从详情接口 get 取到 processInstanceId 后拼接详情页路由。

API

方法 路径 说明 权限标识
POST /finance/reimbursement/create 创建报销单 finance:reimbursement:create
PUT /finance/reimbursement/update 修改报销单 finance:reimbursement:update
DELETE /finance/reimbursement/delete 删除报销单 finance:reimbursement:delete
GET /finance/reimbursement/get 查询报销单详情 finance:reimbursement:query
GET /finance/reimbursement/page 分页查询报销单 finance:reimbursement:query
GET /finance/reimbursement/export-excel 导出 Excel finance:reimbursement:export
POST /finance/reimbursement/submit 提交审批 finance:reimbursement:update-status
POST /finance/reimbursement/archive 归档 finance:reimbursement:archive
GET /finance/reimbursement/approve-process-list 获取可用审批流程列表 finance:reimbursement:query

POST /finance/reimbursement/create

请求参数(JSON body):

参数 类型 必填 说明
no String 否 报销单号,最长 36。**留空(含只填空格)则由后端自动生成**;填了则按自定义单号校验后落库
title String 是 报销标题,最长 200
totalAmount BigDecimal 是 报销总金额
applyDate String 是 申请日期,格式 yyyy-MM-dd
remark String 否 备注,最长 500
blobIds Long[] 否 发票附件的 blobId 列表(可为空数组表示无发票)
items Object[] 是 报销明细,至少 1 行

自定义单号规则(no):

输入 后端行为
不传 / null / 空串 / 纯空格(含全角空格  、制表符) 视为「未填」,由系统按 BX + yyyyMMdd + 6 位流水生成
有值 去掉首尾空格后作为自定义单号;**中间**含空格 / 制表符 / 全角空格则报错
有值但与其他报销单重复 报错 报销单号({})已被其他报销单使用,请更换其他单号,或清空该字段由系统自动生成
有值且中间有空格 报错 报销单号({})中间不能包含空格,请去掉空格后重新输入,或清空该字段由系统自动生成
长度 > 36 校验失败 报销单号长度不能超过 36 个字符

前端在提交前建议先 trim(),把纯空格视为空串再提交,避免用户误以为填了单号。

items 元素:

参数 类型 必填 说明
expenseType Integer 是 费用类型,取字典 finance_expense_type
invoiceNo String 否 发票号码,最长 64
invoiceAmount BigDecimal 是 发票金额
invoiceDate String 否 发票日期,格式 yyyy-MM-dd
drawer String 否 开票方,最长 200
description String 否 费用说明,最长 500

响应:data 为新建报销单的 id(Long)。

PUT /finance/reimbursement/update

请求参数与 create 相同,额外**必填** id(Long)。仅 status=0 草稿 或 status=35 已驳回 可改。

no 在修改时的语义与新增不同:

输入 后端行为
不传 / null / 空串 / 纯空格 保持原单号不变(不会重新生成)
与当前单号相同 视为不变更
有值且与当前不同 校验格式与唯一性后覆盖为新单号

前端编辑态应回显原单号;若用户清空该输入框,提交时传空串即表示「不改单号」,不会被系统重新生成。

响应:data = true。

DELETE /finance/reimbursement/delete

参数 位置 类型 必填 说明
id query Long 是 报销单 id

仅 status=0 草稿 可删。删除时会同时清理明细、发票附件。

GET /finance/reimbursement/get

参数 位置 类型 必填
id query Long 是

响应字段:

字段 类型 说明
id Long 报销单 id
no String 报销单号。可能是后端生成的 BX + yyyyMMdd + 6 位流水(如 BX20260928000001),也可能是用户自定义的单号
title String 报销标题
totalAmount BigDecimal 报销总金额
status Integer 状态,取字典 finance_reimbursement_status
applicantId Long 申请人 id
applicantName String 申请人姓名(后端已解析昵称,前端不用再查)
applyDate String 申请日期 yyyy-MM-dd
processInstanceId String BPM 流程实例 id,未提交时为 null
archiveTime String 归档时间 yyyy-MM-dd HH:mm:ss,未归档为 null
remark String 备注
creator String 创建人 id(字符串形式)
creatorName String 创建人姓名(后端已解析)
createTime String 创建时间
updateTime String 更新时间
items Object[] 报销明细列表
attachmentList Object[] 发票附件列表
hasAttachment Boolean 是否有发票附件

items 元素:id、reimbursementId、expenseType、invoiceNo、invoiceAmount、invoiceDate、drawer、description。

attachmentList 元素:id(blobId)、url(文件地址)、previewURL(预览地址)、name(文件名)、downloadURL(下载地址)、application、storageAttachmentId(附件关联 id)。

GET /finance/reimbursement/page

参数 类型 必填 说明
pageNo Integer 是 页码,从 1 开始
pageSize Integer 是 每页条数
no String 否 报销单号,模糊匹配
title String 否 报销标题,模糊匹配
status Integer 否 状态,精确匹配
applicantId Long 否 申请人 id
applyDate String[] 否 申请日期区间

日期区间传参(易错点):后端字段是数组,需按下面形式传两个值,格式 yyyy-MM-dd(不含时分秒):

  • applyDate[0]=2026-09-01&applyDate[1]=2026-09-30

响应:data.list 为详情字段的数组(含 items、attachmentList),data.total 为总数。

GET /finance/reimbursement/export-excel

查询参数同 page,无分页限制(忽略 pageSize)。响应为文件流,前端按 blob 处理即可。

导出列为:报销单号、报销标题、报销总金额、状态(自动转中文)、申请人、申请日期、归档时间、备注、创建时间、是否上传发票。

POST /finance/reimbursement/submit

参数 位置 类型 必填 说明
id query Long 是 报销单 id
processDefinitionKey query String 是 流程定义 key,取自 approve-process-list

注意:参数在 query string,请求体为空。

响应:data = true。成功后报销单 status 变为 15 审批中,processInstanceId 被回写。

POST /finance/reimbursement/archive

参数 位置 类型 必填
id query Long 是

响应:data = true。成功后 status=40 已归档、archiveTime 被写入。

GET /finance/reimbursement/approve-process-list

无请求参数。

响应:data 为数组,元素形如:

字段 类型 说明
id String 流程定义 id
key String 流程定义 key,提交审批时回传此值
name String 流程名称

若返回空数组,说明「报销审批」分类下**尚无已发布的流程定义**,此时必须禁用「提交审批」按钮并提示用户先到 BPM 流程设计器发布流程。

字段展示规则

字段 展示位置 说明
报销单号 no 列表、详情、编辑表单 可编辑(选填)。新增时留空由系统生成;编辑时留空表示不改单号
报销标题 title 列表(可点开详情)、编辑表单 必填
状态 status 列表、详情 用 DictTag 渲染字典 finance_reimbursement_status,取后端返回的颜色
报销总金额 totalAmount 列表(右对齐)、详情 必填,建议由明细汇总回填
申请人 applicantName 列表、详情 只读,后端已解析姓名
申请日期 applyDate 列表、编辑表单 必填,日期选择器(不带时间)
创建人 creatorName / 创建时间 createTime 列表、详情 只读
归档时间 archiveTime 详情 只读,未归档显示 -
备注 remark 详情、编辑表单 选填
明细行 items 编辑表单(子表格)、详情 列表页可不展示,详情页可折叠展开
明细.费用类型 expenseType 明细表格 用字典 finance_expense_type 渲染
明细.发票金额 invoiceAmount 明细表格(右对齐) 必填
发票附件 attachmentList 编辑表单、详情 只读列表 + 预览/下载;编辑态允许增删(见「注意事项」)
是否有发票 hasAttachment 列表 可展示为「有/无」标签,方便财务扫单

业务规则说明

状态与可用操作矩阵

状态 编辑 删除 提交审批 归档 说明
0 草稿 ✅ ✅ ✅ ❌ 唯一可删除的状态
15 审批中 ❌ ❌ ❌ ❌ 全部只读,等待审批结果
20 已通过 ❌ ❌ ❌ ✅ 仅可归档
35 已驳回 ✅ ❌ ✅ ❌ 可改发票后重新提交;**不可删除**
40 已归档 ❌ ❌ ❌ ❌ 终态,全部只读

校验规则(后端已实现,前端应做同步前置校验以提升体验)

场景 规则 后端返回的提示
创建/修改时 items 为空 至少 1 行明细 报销明细不能为空
创建/修改时 title / totalAmount / applyDate 为空 必填 报销标题不能为空 / 报销总金额不能为空 / 申请日期不能为空
自定义单号与其他报销单重复 拒绝 报销单号({})已被其他报销单使用,请更换其他单号,或清空该字段由系统自动生成
自定义单号中间含空格/制表符/全角空格 拒绝 报销单号({})中间不能包含空格,请去掉空格后重新输入,或清空该字段由系统自动生成
自定义单号长度 > 36 拒绝 报销单号长度不能超过 36 个字符
非草稿/非已驳回状态调 update 拒绝 报销单({})不是草稿或已驳回状态,无法修改,只有草稿和已驳回的报销单可以编辑
非草稿状态调 delete 拒绝 报销单({})不是草稿状态,无法删除
非草稿/非已驳回调 submit 拒绝 提交审批失败,只有草稿或已驳回状态的报销单才能提交
提交时无明细 拒绝 提交审批失败,报销单没有明细行,无法提交
非已通过调 archive 拒绝 归档失败,只有已通过状态的报销单才能归档
报销单不存在 拒绝 报销单不存在

审批结果如何回写状态(前端不用管,仅作了解)

BPM 审批流完成后,后端监听器按结果自动改状态:
- 审批通过 → 20 已通过
- 审批驳回 → 35 已驳回
- 审批取消 → 退回 0 草稿

前端只需在审批通过后重新拉列表/详情即可看到新状态。

注意事项

  1. 附件更新是全量替换,不是增量(最容易踩的坑):update 接口的 blobIds 语义是「该报销单保存后应持有的**全部**附件」。因此编辑态必须把**已有附件的 blobId 与新增附件的 blobId 合并成一个数组**一起提交;只传新增的那几个,会把原有发票全部删掉。传空数组 [] 表示清空全部附件;不传(null)表示不动附件。

  2. 上传与保存分离:上传走 /system/storage-blob/upload 拿 blobId,保存走 create / update 提交 blobIds。禁止调用 /infra/file/upload、/infra/file/presigned-url,也禁止在业务表/表单里存 fileUrl 之类的文件地址字段。

  3. 删除时附件会一并清理(后端已处理),前端不需要额外调附件删除接口。但要注意:**上传后未保存就关闭弹窗**的孤儿文件不会被自动清理,建议前端在关闭时提示,或复用同一批 blobId 下次保存。

  4. submit 的参数在 query,请求体为空。若按 JSON body 传会取不到 id 与 processDefinitionKey。

  5. 审批流程 key 严禁写死:必须先调 approve-process-list,把返回的 name+key 渲染成下拉(可参考 ERP 付款单的 process-select-modal)。列表为空时禁用提交按钮。

  6. 「已驳回」可编辑,但不可删除:后端 update 放行 status=0 草稿 与 status=35 已驳回;delete 仍只放行草稿。因此编辑按钮在 0/35 显示,删除按钮只在 0 显示。

  7. totalAmount 与明细汇总不联动校验:后端不做一致性校验,总金额由前端负责回填与展示,用户可手改。若要求强一致,需后端补校验规则。

  8. 日期区间筛选传参格式是 yyyy-MM-dd(如 applyDate[0]=2026-09-01),**不要带时分秒**,否则后端解析失败报 400。注意列表日期区间选择器必须显式设置 valueFormat: 'YYYY-MM-DD',项目通用的 getRangePickerDefaultProps() 默认是 YYYY-MM-DD HH:mm:ss,直接用会踩这个坑。

  9. 列表页请不要默认展开 items 与 attachmentList:分页接口对每行都会查明细和附件,行数多时开销较大;建议列表只展示主表字段(含 hasAttachment),明细与附件放到详情页按需加载。

  10. 报销单号可自定义,留空则由后端生成:表单里保留「报销单号」输入框,占位提示写「留空则由系统自动生成」。提交前请 trim(),纯空格要当作未填提交(后端也会再 trim 一次兜底)。生成规则为 Redis 序列 BX + yyyyMMdd + 6 位流水(每日重置)。**不要在编辑态用空串覆盖已有单号**——后端把空值解释为「保持不变」,不会重新生成。

  11. 明细行提交时不要带 id:后端保存明细是「先删后增」全量覆盖,而明细删除是**软删除**,若回传了原行 id,重新插入时会与已软删的行主键冲突。前端提交前必须剥掉每行的 id 与 reimbursementId,只保留 expenseType、invoiceNo、invoiceAmount、invoiceDate、drawer、description。

  12. 审批详情页由 BPM 流程设计器挂载:BPM 审批中心通过流程定义的 formCustomViewPath 动态加载业务详情页(registerComponent 按路径匹配),因此 BPM 流程设计器里必须把「自定义表单路径」配置为 /finance/reimbursement/detail/approve/index.vue,否则审批中心里看不到报销单内容。