模块:
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 已归档(终态)
数据带入要点
create / update 一次性提交(items 数组),后端按「先删后增」全量覆盖。前端不需要逐行调接口。/system/storage-blob/upload 拿到 blobId;blobId 放进 blobIds 一起提交,由后端统一绑定到 recordType=finance_reimbursement + recordId=报销单id。totalAmount 与 items[].invoiceAmount 之和是否相等,直接入库。若要保证一致,请前端在 items 变化时把 invoiceAmount 求和回填 totalAmount(用户仍可手改)。approve-process-list:前端不得写死流程 key,必须实时取列表后由用户选择(后端在提交时会按所选 key 创建流程实例)。processInstanceId 跳转,前端应从详情接口 get 取到 processInstanceId 后拼接详情页路由。| 方法 | 路径 | 说明 | 权限标识 |
|---|---|---|---|
| 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 |
请求参数(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)。
请求参数与 create 相同,额外**必填** id(Long)。仅 status=0 草稿 或 status=35 已驳回 可改。
no 在修改时的语义与新增不同:
| 输入 | 后端行为 |
|---|---|
不传 / null / 空串 / 纯空格 |
保持原单号不变(不会重新生成) |
| 与当前单号相同 | 视为不变更 |
| 有值且与当前不同 | 校验格式与唯一性后覆盖为新单号 |
前端编辑态应回显原单号;若用户清空该输入框,提交时传空串即表示「不改单号」,不会被系统重新生成。
响应:data = true。
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| id | query | Long | 是 | 报销单 id |
仅 status=0 草稿 可删。删除时会同时清理明细、发票附件。
| 参数 | 位置 | 类型 | 必填 |
|---|---|---|---|
| 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)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| 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 为总数。
查询参数同 page,无分页限制(忽略 pageSize)。响应为文件流,前端按 blob 处理即可。
导出列为:报销单号、报销标题、报销总金额、状态(自动转中文)、申请人、申请日期、归档时间、备注、创建时间、是否上传发票。
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| id | query | Long | 是 | 报销单 id |
| processDefinitionKey | query | String | 是 | 流程定义 key,取自 approve-process-list |
注意:参数在 query string,请求体为空。
响应:data = true。成功后报销单 status 变为 15 审批中,processInstanceId 被回写。
| 参数 | 位置 | 类型 | 必填 |
|---|---|---|---|
| id | query | Long | 是 |
响应:data = true。成功后 status=40 已归档、archiveTime 被写入。
无请求参数。
响应: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 草稿
前端只需在审批通过后重新拉列表/详情即可看到新状态。
附件更新是全量替换,不是增量(最容易踩的坑):update 接口的 blobIds 语义是「该报销单保存后应持有的**全部**附件」。因此编辑态必须把**已有附件的 blobId 与新增附件的 blobId 合并成一个数组**一起提交;只传新增的那几个,会把原有发票全部删掉。传空数组 [] 表示清空全部附件;不传(null)表示不动附件。
上传与保存分离:上传走 /system/storage-blob/upload 拿 blobId,保存走 create / update 提交 blobIds。禁止调用 /infra/file/upload、/infra/file/presigned-url,也禁止在业务表/表单里存 fileUrl 之类的文件地址字段。
删除时附件会一并清理(后端已处理),前端不需要额外调附件删除接口。但要注意:**上传后未保存就关闭弹窗**的孤儿文件不会被自动清理,建议前端在关闭时提示,或复用同一批 blobId 下次保存。
submit 的参数在 query,请求体为空。若按 JSON body 传会取不到 id 与 processDefinitionKey。
审批流程 key 严禁写死:必须先调 approve-process-list,把返回的 name+key 渲染成下拉(可参考 ERP 付款单的 process-select-modal)。列表为空时禁用提交按钮。
「已驳回」可编辑,但不可删除:后端 update 放行 status=0 草稿 与 status=35 已驳回;delete 仍只放行草稿。因此编辑按钮在 0/35 显示,删除按钮只在 0 显示。
totalAmount 与明细汇总不联动校验:后端不做一致性校验,总金额由前端负责回填与展示,用户可手改。若要求强一致,需后端补校验规则。
日期区间筛选传参格式是 yyyy-MM-dd(如 applyDate[0]=2026-09-01),**不要带时分秒**,否则后端解析失败报 400。注意列表日期区间选择器必须显式设置 valueFormat: 'YYYY-MM-DD',项目通用的 getRangePickerDefaultProps() 默认是 YYYY-MM-DD HH:mm:ss,直接用会踩这个坑。
列表页请不要默认展开 items 与 attachmentList:分页接口对每行都会查明细和附件,行数多时开销较大;建议列表只展示主表字段(含 hasAttachment),明细与附件放到详情页按需加载。
报销单号可自定义,留空则由后端生成:表单里保留「报销单号」输入框,占位提示写「留空则由系统自动生成」。提交前请 trim(),纯空格要当作未填提交(后端也会再 trim 一次兜底)。生成规则为 Redis 序列 BX + yyyyMMdd + 6 位流水(每日重置)。**不要在编辑态用空串覆盖已有单号**——后端把空值解释为「保持不变」,不会重新生成。
明细行提交时不要带 id:后端保存明细是「先删后增」全量覆盖,而明细删除是**软删除**,若回传了原行 id,重新插入时会与已软删的行主键冲突。前端提交前必须剥掉每行的 id 与 reimbursementId,只保留 expenseType、invoiceNo、invoiceAmount、invoiceDate、drawer、description。
审批详情页由 BPM 流程设计器挂载:BPM 审批中心通过流程定义的 formCustomViewPath 动态加载业务详情页(registerComponent 按路径匹配),因此 BPM 流程设计器里必须把「自定义表单路径」配置为 /finance/reimbursement/detail/approve/index.vue,否则审批中心里看不到报销单内容。