Files
workspace/code/fms/文件组件设计方案.md
T
2026-08-04 22:31:52 +08:00

219 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FMS 文件管理组件设计方案(三栏布局)
> 状态:待执行(计划稿)
> 目标:参考旧系统 `g3file`,为新 FMS 系统设计可复用的文件管理组件。
> 布局:左 = 分类树,中 = 文件列表 + 操作区,右 = 预览区。
---
## 1. 背景与新旧差异
旧系统 `g3file` 是分两层的组件:
- 参数化通用库 `file-preview/g3-file`(基于 Element Plus + 阿里云 OSS),支持 `modelValue`、`module-id`、`father`、`cate-id`、`readonly`、`search-condition`、`upload-config.aliyun`、`preview-url`、`global-permission`、`iconfont` 及多种回调。
- 业务包装层 `g3hd/frontend/src/components/g3-file/index.vue`,业务页只用一行调用:`<G3File v-model="showFile" :father="id" module-id="oa_notices" />`。
新 FMS 的差异与决策:
| 维度 | 旧系统 | 新系统决策 |
|------|--------|-----------|
| 框架 | Vue3 + Element Plus | Vue3 + **antdv-next**(自动导入,勿手导) |
| 上传存储 | 阿里云 OSS | 后端**本地磁盘**,文件名 `{subid}.{ext}`;预留 `OssStorageAdapter` 抽象便于将来切换 |
| moduleid | 存字符串业务 code(如 `oa_notices`) | `bf_files.mx_moduleid` 为 **bigint 外键 → `s_module.b_id`** |
| 预览 | 自研/第三方 | 复用已安装的 `@file-viewer/vue3-full`(`FileViewer`,dashboard 已验证 `withFullViewerOptions()`) |
| 调用方式 | `module-id` 字符串 | **`module-code`**(如 `oa_notices`);组件内解析为 `b_id` 写入 `mx_moduleid` |
### 关于 moduleid 的关键决策
- **保留 `mx_moduleid` 字段**,但它是**派生字段**,由组件根据 `module-code` 查 `s_module.b_id` 后写入。
- 业务页**不单独传 moduleid**,只传 `module-code` 与 `father`。组件内部完成 code → b_id 解析并随上传/保存一并提交。
- 说明:旧系统把字符串 code 直接当 moduleid 存,新系统 `mx_moduleid` 是指向 `s_module.b_id` 的 bigint 外键,二者语义不同;用 `module-code` 解析可兼容旧习惯又满足新外键约束。
---
## 2. 数据库现状(已存在,无需新建表)
表 `bf_files` / `bf_files_cateid` / `bf_files_type` 已在迁移 `017_create_contract_file_tables.sql` 定义,字段如下(节选相关项):
`bf_files`:
- `subid` bigint(主键,Snowflake ID)
- `father` bigint(业务单据 ID,如合同/通知 b_id)
- `mx_xh` int(显示序号)
- `mx_filename` nvarchar(500)(原始文件名)
- `mx_filesize` varchar(50)
- `mx_fileext` varchar(50)
- `mx_mapfilename` nvarchar(500)(**新系统存服务端存储文件名,如 `1234567890.pdf`,不再存阿里云 URL**)
- `mx_moduleid` bigint(FK → `s_module.b_id`,派生字段)
- `mx_cutfilename`、`mx_cutfilesize`(缩略图,可选)
- `mx_cate_id` bigint(→ `bf_files_cateid.b_id`,分类)
- `mx_bz` nvarchar(500)
- `mx_orderid`、`mx_sh`、`mx_shdatetime`、`mx_shuser_id`
- `mx_inputuser_id`、`mx_inputdatetime`、`mx_updateuser_id`、`mx_updatedatetime`
索引:`IX_bf_files_father`、`IX_bf_files_mx_cate_id`、`IX_bf_files_mx_shuser_id`。
`bf_files_cateid`:`b_id`、`b_name`、`b_xh`、`b_bz`、`b_canuse`、`b_files_type`、`b_code`(唯一)等。
`bf_files_type`:`b_id`、`b_name`、`b_module_id`、`b_module_name`、`b_xh` 等。
> 注意:bigint ID(subid / father / mx_moduleid / mx_cate_id)经后端返回前端时**必须序列化为字符串**。
---
## 3. 后端新增文件端点(按开发规范例外处理)
通用端点(`/api/data/...`)无法处理二进制流与 multipart 上传,因此按规范例外新增**与业务表无关的 3 个通用文件端点**。需在新增前于回复中说明原因(已在本文 §1 论证)。
### 3.1 `POST /api/file/upload`
- 请求:`multipart/form-data`
- `file`(必填,二进制)
- `father`(必填,bigint 字符串,业务单据 ID)
- `moduleCode`(必填,如 `oa_notices`)
- `cateId`(可选,bigint 字符串 → `mx_cate_id`)
- `bz`(可选)
- 处理流程:
1. 校验 JWT(过 `JwtAuthFilter`),取 `OrgContext.orgId` 选库。
2. 用 `IdGenerator` 生成 `subid`(Snowflake)。
3. 解析 `moduleCode` → `s_module.b_id`(查 `s_module` 表;查不到则报错 400)。
4. 计算扩展名 `ext`、存储名 `{subid}.{ext}`,写字节到 `fms.file.storage-dir`。
5. **只写磁盘文件,不写 `bf_files` 行**(行由前端用 `saveObjectApi` 批量保存,便于统一事务/权限)。
- 响应(JSON):
```json
{
"subid": "1234567890",
"storedName": "1234567890.pdf",
"mx_filename": "合同.pdf",
"mx_filesize": "102400",
"mx_fileext": "pdf",
"mx_mapfilename": "1234567890.pdf"
}
```
### 3.2 `GET /api/file/{subid}`
- 查 `bf_files` 取 `mx_mapfilename`,从 `storage-dir` 流式返回字节。
- 响应头:`Content-Type`(按扩展名映射)、`Content-Disposition: inline; filename*=UTF-8''<mx_filename>`(inline 便于浏览器/pdf 预览)。
- 需过 `JwtAuthFilter`(带 Token 才能取)。
### 3.3 `DELETE /api/file/{subid}`
- 删磁盘文件 + 删 `bf_files` 行(业务页也可改为走 `saveObjectApi` 的 `deletes[]`,二选一;推荐端点方式以便同时清理磁盘)。
- 返回 `{ success: true }`。
### 3.4 配置
`fms-api/src/main/resources/application.yaml` 新增:
```yaml
fms:
file:
storage-dir: ./data/files # 绝对或相对(相对应用工作目录),需 git-ignore
```
`storage-dir` 目录加入 `.gitignore`。
---
## 4. 前端组件设计(三栏布局)
目录:`fms-vue/src/components/file/`
```
FileManager.vue 三栏容器(左树 / 中列表操作 / 右预览)
FileCategoryTree.vue 左栏:分类树(虚拟根“全部附件” + bf_files_cateid 节点)
FileList.vue 中栏:文件列表 + 工具栏(上传/下载/删除/刷新)
FileUpload.vue a-upload 包装(调用 fileService.upload)
```
复用:`@file-viewer/vue3-full` 的 `FileViewer` 用于右栏预览;`antdv-next` 的 `a-upload` / `a-table` / `a-tree` / `a-button` 等(自动导入)。
服务层:`fms-vue/src/services/fileService.js`。
### 4.1 FileManager.vue(容器)
- 三栏 flex 布局:左 `FileCategoryTree`(宽 ~220px,受 `showTree` 控制),中 `FileList`(flex:1),右预览区(宽 ~40% 或固定 ~480px,仅当选中文件时显示)。
- 选中文件(来自 `FileList` 的 `select` 事件)→ 右栏渲染 `FileViewer` 并传入预览 URL(`fileService.getContentUrl(subid)`)。
- 负责 `moduleCode` → `moduleId`(b_id) 解析:挂载时调 `loadModuleConfiguration` / 通用 `loaddata` 查 `s_module` 得到 `b_id`,下传 `FileList` 备用(上传时带回)。
- 工具栏按钮(上传/下载/删除)按 `moduleCode` 查 `b_user_power` 控制显隐/禁用(前端权限,沿用现有 `permissions` store 模式)。
**Props**
| 名称 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `modelValue` | Boolean | 是 | — | 控制显隐(v-model) |
| `moduleCode` | String | 是 | — | 模块 code,如 `oa_notices` |
| `father` | [String, Number] | 是 | — | 业务单据 ID(bigint 字符串) |
| `moduleId` | [String, Number] | 否 | — | 已解析的 `s_module.b_id`;不传则组件内解析 |
| `cateId` | [String, Number] | 否 | — | 初始分类过滤 |
| `categories` | Array | 否 | — | 预置分类列表,避免请求树 |
| `readonly` | Boolean | 否 | false | 只读(隐藏上传/删除) |
| `showTree` | Boolean | 否 | true | 是否显示左栏分类树 |
**Events**
| 名称 | 载荷 | 说明 |
|------|------|------|
| `update:modelValue` | Boolean | v-model |
| `uploaded` | fileRow | 上传完成(含回写 bf_files 后的行) |
| `deleted` | subid | 删除完成 |
| `selected` | fileRow \| null | 选中文件变化(驱动右栏预览) |
| `error` | Error | 异常 |
### 4.2 FileCategoryTree.vue(左栏)
- 虚拟根节点“全部附件”(值为 `null` 或 `'__all__'`),下挂 `bf_files_cateid` 节点(来自 `loaddata` 查 `bf_files_cateid`,按 `b_xh` 排序)。
- 选中分类 emit `select(cateId)`,供 `FileList` 过滤。
- 若 `categories` prop 已提供则直接用,否则自行加载。
### 4.3 FileList.vue(中栏)
- `a-table` 展示当前 `father` +(可选)`cateId` 下的 `bf_files` 行(走 `pageDataApi` / `loadDataApi`,`view_name='bf_files'`)。
- 工具栏:`FileUpload`(上传)、下载、删除、刷新。
- 行选中 → emit `select(row)` → 容器驱动右栏。
- 上传完成后调用 `saveObjectApi` 写 `bf_files` 行(inserts: `[{table:'bf_files', key_field:'subid', ...}]`,带 `father`/`mx_moduleid`/`mx_filename`/`mx_filesize`/`mx_fileext`/`mx_mapfilename`/`mx_cate_id`)。
- 删除:调 `fileService.remove(subid)`(或 `saveObjectApi` deletes + 端点删磁盘)。
### 4.4 FileUpload.vue
- 包装 `a-upload`(`:before-upload` 拦截,手动调 `fileService.upload`)。
- 入参:`father`、`moduleCode`、`moduleId`、`cateId`;上传成功后把响应行 emit 给 `FileList` 继续写元数据。
### 4.5 fileService.js
封装以下方法(基于 `src/services/http.js`):
- `upload({file, father, moduleCode, cateId, bz})` → `POST /api/file/upload`(multipart)
- `getContentUrl(subid)` → 返回 `GET /api/file/{subid}` 的可访问 URL(需带 Token;若 axios 拦截器不自动带,则改用带 Token 的 `fetch`/blob 下载后 `URL.createObjectURL` 喂给 `FileViewer`)
- `remove(subid)` → `DELETE /api/file/{subid}`
- `listFiles({father, cateId})` → `pageDataApi`(`bf_files`)
- `saveMeta(rows)` → `saveObjectApi`
> 预览 URL 处理:因 `FileViewer` 需要可直接访问的资源地址,而 `/api/file/{subid}` 需鉴权,推荐在 `fileService` 内用带 `Authorization` 的 `fetch` 取 blob 再 `createObjectURL`,避免 Token 泄漏到 URL。
---
## 5. 业务页调用示例
```vue
<!-- 业务页(如通知管理) -->
<FileManager
v-model="showFile"
module-code="oa_notices"
:father="rowId"
/>
```
- 业务页**只需传 `module-code` 与 `father`**,不再传 moduleid。
- 只读场景:`readonly` + `show-tree` 可组合使用。
---
## 6. 落地步骤清单(执行顺序)
1. **后端**
- [ ] `application.yaml` 增加 `fms.file.storage-dir`,并把该目录加入 `.gitignore`。
- [ ] 新建 `FileController`(或并入现有 controller 包),实现 `upload` / `GET {subid}` / `DELETE {subid}` 三个端点,复用 `OrgContext` + `DbUtils` + `IdGenerator`。
- [ ] 实现本地磁盘存储(`StorageService`,预留 `OssStorageAdapter` 接口)。
- [ ] `moduleCode → b_id` 解析逻辑(查 `s_module`)。
2. **前端服务**
- [ ] `src/services/fileService.js` 五个方法。
3. **前端组件**
- [ ] `FileCategoryTree.vue`(左树)
- [ ] `FileList.vue`(中栏列表+操作,含写元数据/删除)
- [ ] `FileUpload.vue`(a-upload 包装)
- [ ] `FileManager.vue`(三栏容器 + 权限 + moduleCode 解析 + 右栏预览)
4. **集成验证**
- [ ] 选一个业务页(如通知 `oa_notices`)挂载 `<FileManager>`,验证上传/列表/分类过滤/预览/删除全链路。
- [ ] 确认 bigint ID 前端为字符串、预览 URL 鉴权正常。
---
## 7. 注意事项(沿用开发规范)
- 不新增业务专属数据端点(文件端点属通用基础设施例外,已在 §1 论证)。
- 不手动 `import` antdv-next 组件(vite resolver 已自动导入)。
- 不引入新 vue-i18n;文案沿用 DB 驱动 i18n(必要时走 `b_i18n`)。
- 改动保持聚焦,不触碰无关文件;保留用户未提交的工作。
- 交付时说明是否运行了测试、是否执行了 build(默认不主动 build/起服务,除非用户要求)。