所有模块的文件上传已从 fileUrl 字符串字段改为 system_storage_attachment 中间表机制:
| 变更 | 旧方式 | 新方式 |
|---|---|---|
| 上传 | 直接存 fileUrl | POST /system/storage-blob/upload → 获取 blobId |
| 保存 | SaveReqVO.fileUrl | SaveReqVO.blobIds (List<Long>) |
| 查询 | RespVO.fileUrl | RespVO.attachmentList (List<StorageBlob>) |
| 删除 | 无 | 后端 Service 自动同步删除附件 |
| 页面 | 目录 | 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 |
| 页面 | 目录 | recordType |
|---|---|---|
| 销售报价单 | src/views/crm/saleQuotation/ |
crm_sale_quotation |
| 跟进记录 | src/views/crm/followup/ |
crm_follow_up_record |
| 页面 | 目录 | recordType |
|---|---|---|
| 设计资料 | src/views/mes/pd/project/modules/document-form.vue |
mes_pd_document |
| 方法 | 路径 | 说明 |
|---|---|---|
| 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
所有模块的创建/更新接口均新增 blobIds 参数:
| 接口 | 新增参数 |
|---|---|
| 所有模块 create/update | blobIds: number[] |
所有模块的详情查询接口均新增 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 }
修改 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 |
新增/编辑模式需要添加文件上传组件,流程:
用户选择文件 → 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
在详情/查看模式下展示附件列表(只读):
<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(性能考虑),仅详情接口返回 |
src/views/hrm/xxx/modules/form.vueblobIds 随表单数据一起发送blobIds 不为 null 时后端重新绑定src/views/hrm/attendance/exception/modules/form.vuesrc/views/hrm/salary/payment/index.vuesrc/views/hrm/salary/payment/modules/detail.vuesrc/views/crm/saleQuotation/modules/form.vueuseVbenModal + useVbenForm,需要在上传区域加入 a-uploadsrc/views/crm/followup/modules/form.vuefileUrls: string[] 存储附件地址,现在改为 blobIdssrc/views/mes/pd/project/modules/document-form.vuefileUrl 是资料文件自身的地址,需确认是否也需要改为 attachment 方式┌─────────┐ ┌──────────┐ ┌──────────┐
│ 前端 │ │ system │ │ 业务模块 │
│ │ │ 模块 │ │ │
└────┬────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1.选择文件 │ │
│──────────────>│ │
│ 2.返回 blobId │ │
│<──────────────│ │
│ │ │
│ 3.save(blobIds) │
│──────────────────────────────>│
│ │ │
│ │ 4.bind(blobIds)──> 内部调用
│ │<──────────────│
│ │ │
│ 5.返回成功 │ │
│<──────────────────────────────│
│ │ │
│ 6.查看详情 │ │
│──────────────────────────────>│
│ │ │
│ 7.返回 attachmentList │
│<──────────────────────────────│
deleteAttachments([attachmentId]) 删除单个附件,但不强制,因为保存时 blobIds 不为 null 会触发后端重新绑定MesPdDocumentDO 原有的 fileUrl 字段已被删除,如果这个字段是作为业务文件地址(而非附件),可能需要单独评估是否需要保留文件存储fileUrls: string[](数组),现在改为 blobIds: number[],接口层面变化较大