20260730231110

This commit is contained in:
oneao committed 2026-07-30 23:11:10 +08:00
1 parent 4929b78434
commit 318fa0d9e1
23 files changed
+4026 -506

No files matched your search

+218
View File
@@ -0,0 +1,218 @@
# 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/起服务,除非用户要求)。