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

文件上传改造 - 前端联调方案

后端变更摘要

所有模块的文件上传已从 fileUrl 字符串字段改为 system_storage_attachment 中间表机制:

变更 旧方式 新方式
上传 直接存 fileUrl POST /system/storage-blob/upload → 获取 blobId
保存 SaveReqVO.fileUrl SaveReqVO.blobIds (List<Long>)
查询 RespVO.fileUrl RespVO.attachmentList (List<StorageBlob>)
删除 后端 Service 自动同步删除附件

涉及页面

HRM 模块(5个页面)

页面 目录 recordType
请假申请 src/views/hrm/leave/ hrm_leave_application
离职申请 src/views/hrm/resignation/ hrm_resignation_application
调岗申请 src/views/hrm/transfer/ hrm_transfer_application
考勤异常申请 src/views/hrm/attendance/exception/ hrm_attendance_exception
薪资发放 src/views/hrm/salary/payment/ hrm_salary_payment

CRM 模块(2个页面)

页面 目录 recordType
销售报价单 src/views/crm/saleQuotation/ crm_sale_quotation
跟进记录 src/views/crm/followup/ crm_follow_up_record

MES 模块(1个页面)

页面 目录 recordType
设计资料 src/views/mes/pd/project/modules/document-form.vue mes_pd_document

API 说明

通用文件 API(已有,无需新增)

方法 路径 说明
POST /system/storage-blob/upload 上传文件(multipart,files 参数)
GET /system/storage-attachment/list 查询附件列表(recordType + recordId
POST /system/storage-attachment/bind 绑定附件到业务记录
DELETE /system/storage-attachment/delete 批量删除附件

前端已有对应的 API 封装在 src/api/system/storage/index.ts
- uploadFile(files)StorageBlob[]
- listAttachments({ recordType, recordId })StorageBlob[]
- bindAttachments({ application, recordType, recordId, blobItems })boolean
- deleteAttachments(ids)boolean

各接口 SaveReqVO 新增字段

所有模块的创建/更新接口均新增 blobIds 参数:

接口 新增参数
所有模块 create/update blobIds: number[]

各接口 RespVO 新增字段

所有模块的详情查询接口均新增 attachmentList 字段:

接口 新增字段 类型
GET /get(详情) attachmentList StorageBlob[]

StorageBlob 结构:
typescript interface StorageBlob { id: number; // blob ID url?: string; // 文件访问 URL previewURL?: string; // 预览地址 name?: string; // 原文件名 downloadURL?: string; // 下载地址 application?: string; // 文件用途 storageAttachmentId?: number; // 附件关联 ID }


前端修改步骤(每个模块通用)

步骤 1:修改 API 类型定义

修改 src/api/{module}/index.ts 中的接口定义:

// ✅ 引入 StorageBlob 类型
import type { SystemStorageApi } from '#/api/system/storage';

export namespace XxxApi {
  export interface Xxx {
    // ... 其他字段不变 ...

    // ❌ 删除:fileUrl?: string;
    // ✅ 新增:
    blobIds?: number[];                          // 保存时传入的附件 blobId 列表
    attachmentList?: SystemStorageApi.StorageBlob[];  // 查询时返回的附件列表
  }
}

涉及的文件:

文件 删除字段 新增字段
src/api/hrm/leave/index.ts fileUrl blobIds, attachmentList
src/api/hrm/resignation/index.ts fileUrl blobIds, attachmentList
src/api/hrm/transfer/index.ts fileUrl blobIds, attachmentList
src/api/hrm/attendance/exception/index.ts fileUrl blobIds, attachmentList
src/api/hrm/salary/payment/index.ts fileUrl blobIds, attachmentList
src/api/crm/saleQuotation/index.ts fileUrl blobIds, attachmentList
src/api/crm/followup/index.ts fileUrls blobIds, attachmentList
src/api/mes/pd/document/index.ts fileUrl blobIds, attachmentList

步骤 2:修改表单页面(form.vue)

新增/编辑模式需要添加文件上传组件,流程:

用户选择文件 → uploadFile(files) 上传 → 获得 blobId[]
→ 保存时将 blobIds 发给后端 → 后端自动 bindAttachments

核心代码模式:

<script lang="ts" setup>
import { ref } from 'vue';
import { uploadFile } from '#/api/system/storage';
import type { UploadChangeParam } from 'ant-design-vue';

const blobIds = ref<number[]>([]);
const fileList = ref([]);

// 文件上传处理
async function handleUpload(info: UploadChangeParam) {
  if (info.file.status === 'done') {
    // 文件上传成功,收集 blobId
    const blobs = info.file.response;
    if (blobs?.length) {
      blobIds.value.push(blobs[0].id);
    }
  }
}

// 保存时
async function handleSave() {
  const data = await formApi.getValues();
  data.blobIds = blobIds.value;  // 将 blobIds 传给后端
  await createXxx(data);
}

// 编辑时回显已有附件
async function loadDetail(id: number) {
  const detail = await getXxx(id);
  if (detail.attachmentList) {
    fileList.value = detail.attachmentList.map(item => ({
      uid: String(item.id),
      name: item.name || '',
      status: 'done',
      url: item.url,
    }));
    blobIds.value = detail.attachmentList.map(item => item.id);
  }
}
</script>

<template>
  <a-upload
    v-model:file-list="fileList"
    :action="`${baseURL}/system/storage-blob/upload`"
    multiple
    @change="handleUpload"
  >
    <a-button>
      <upload-outlined />
      上传附件
    </a-button>
  </a-upload>
</template>

或者使用已经封装的 uploadFile 函数配合手动上传:
- 推荐方式:在提交前先调用 uploadFile() 上传所有文件,拿到 blobIds 后一起提交
- 备选方式:使用 a-upload 组件的自动上传,在 @change 中收集 blobId

步骤 3:修改详情展示

在详情/查看模式下展示附件列表(只读):

<template>
  <!-- 附件展示 -->
  <div v-if="attachmentList?.length">
    <div class="text-gray-500 mb-2">附件</div>
    <div v-for="item in attachmentList" :key="item.id">
      <a :href="item.url" target="_blank">{{ item.name }}</a>
    </div>
  </div>
</template>

字段展示规则

字段 展示位置 说明
blobIds 表单(不展示) 隐藏字段,保存时传给后端
attachmentList 详情页、表单编辑回显 显示文件列表,支持预览/下载

业务规则说明

场景 规则
创建时 先调用 uploadFile() 上传文件拿 blobId,再把 blobIds 传给 create 接口
更新时 可以新增/删除附件,后端收到 blobIds 不为 null 时先删后绑
查看时 详情接口返回 attachmentList,直接渲染
删除时 后端自动处理附件删除,前端无需额外操作
列表/分页 不返回 attachmentList(性能考虑),仅详情接口返回

各模块特殊说明

HRM 请假/离职/调岗

  • 表单页:src/views/hrm/xxx/modules/form.vue
  • 这三个模块结构相同,参考请假已完成的集成方式
  • 创建时 blobIds 随表单数据一起发送
  • 更新时 blobIds 不为 null 时后端重新绑定

HRM 考勤异常申请

  • 表单页:src/views/hrm/attendance/exception/modules/form.vue
  • 目前只有 create 和 update、delete,注意 update 时也需要处理附件重绑

HRM 薪资发放

  • 列表页:src/views/hrm/salary/payment/index.vue
  • 详情页:src/views/hrm/salary/payment/modules/detail.vue
  • 薪资发放是后端自动生成的,没有创建/编辑表单
  • 只需在详情页展示 attachmentList
  • 删除时后端自动同步删除附件

CRM 销售报价单

  • 表单页:src/views/crm/saleQuotation/modules/form.vue
  • 列表页的 items 是物料明细,附件展示在详情/编辑弹窗中
  • 表单已经使用 useVbenModal + useVbenForm,需要在上传区域加入 a-upload

CRM 跟进记录

  • 表单页:src/views/crm/followup/modules/form.vue
  • 之前使用 fileUrls: string[] 存储附件地址,现在改为 blobIds
  • 原来可能有图片/附件上传区域,需要改为 storage API

MES 设计资料

  • 表单页:src/views/mes/pd/project/modules/document-form.vue
  • 设计资料本身就是文件上传场景(资料类型 + 文件上传)
  • 注意:设计资料的"文件"是业务内容,而 attachment 是附加的参考文件
  • 原有的 fileUrl 是资料文件自身的地址,需确认是否也需要改为 attachment 方式

文件上传流程时序

┌─────────┐     ┌──────────┐     ┌──────────┐
│ 前端     │     │ system   │     │ 业务模块  │
│          │     │ 模块     │     │          │
└────┬────┘     └────┬─────┘     └────┬─────┘
     │               │               │
     │ 1.选择文件     │               │
     │──────────────>│               │
     │ 2.返回 blobId │               │
     │<──────────────│               │
     │               │               │
     │ 3.save(blobIds)               │
     │──────────────────────────────>│
     │               │               │
     │               │ 4.bind(blobIds)──> 内部调用
     │               │<──────────────│
     │               │               │
     │ 5.返回成功     │               │
     │<──────────────────────────────│
     │               │               │
     │ 6.查看详情     │               │
     │──────────────────────────────>│
     │               │               │
     │ 7.返回 attachmentList         │
     │<──────────────────────────────│

注意事项

  1. 上传时机:文件上传应独立于业务保存,先上传拿 blobId,再保存业务数据
  2. 编辑回显:编辑时必须通过 GET /get 接口拿到 attachmentList 回显到上传组件
  3. 大文件处理:如果文件较大,需要在保存前等待上传完成(loading 状态)
  4. 删除附件:前端可以通过 deleteAttachments([attachmentId]) 删除单个附件,但不强制,因为保存时 blobIds 不为 null 会触发后端重新绑定
  5. 薪资发放特殊:薪资发放无创建/编辑表单,只需在其详情页展示 attachmentList
  6. MES 设计资料特殊MesPdDocumentDO 原有的 fileUrl 字段已被删除,如果这个字段是作为业务文件地址(而非附件),可能需要单独评估是否需要保留文件存储
  7. 跟进记录特殊:跟进记录之前用 fileUrls: string[](数组),现在改为 blobIds: number[],接口层面变化较大
  8. 列表页不展示附件:为性能考虑,列表/分页接口不返回 attachmentList,只在详情接口返回