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

12 KiB
Raw Blame History

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):
    {
      "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 新增:

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. 业务页调用示例

<!-- 业务页(如通知管理) -->
<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/起服务,除非用户要求)。