# 项目文档:配送合同附件上传 v1.0 ## 1. 项目概述 本功能将平台总后台配送合同表单中的“附件地址”文本框升级为受控 PDF 上传控件。合同草稿允许不上传附件,但启用合同前必须存在真实、可读取的受控 PDF。功能不修改数据库结构,继续复用 `gasorder_contract.file_uri`,并保持原有合同 JSON 接口和通用 `/upload/file` 接口兼容。 主要能力: - 单文件 PDF 上传,最大 10 MiB; - 点击保存时上传并绑定,失败时立即清理临时文件; - 草稿合同支持替换和移除,非草稿只能预览; - 通过合同菜单权限鉴权预览,不暴露内部文件 URI; - 合同启用时验证受控路径和磁盘文件; - 通过签名收据和附件版本标识防止伪造路径与并发覆盖; - 上传、绑定、替换、移除、下载和清理写入应用日志。 技术栈:Go、Gin、GORM、Vue 3、TypeScript、Arco Design、Vite。 运行要求:后端必须正常加载 JWT 密钥;文件目录由 `HEQI_UPLOAD_DIR` 指定,未指定时使用 `runtime/uploads`。 ## 2. 目录结构 ```text platforms/ ├── backend/api/internal/ │ ├── logic/platform/gasorder/ │ │ ├── contract_attachment.go # 合同附件上传、签名、绑定、预览、清理和审计 │ │ ├── contract_attachment_test.go # PDF、签名、路径和版本边界测试 │ │ └── gasorder.go # 合同创建、草稿更新和启用校验接入 │ └── routers/ │ ├── platform.go # 合同附件专用路由 │ └── platform_contract_attachment_test.go # 路由注册测试 ├── frontend/platform_admin/src/ │ ├── api/ │ │ ├── contract-attachment.ts # 上传、清理和预览 API 客户端 │ │ └── resources.ts # 合同附件字段类型和启用动作状态 │ └── views/resource/ │ ├── ContractAttachmentField.vue # 独立 PDF 选择、拖拽、键盘与操作按钮控件 │ ├── ResourceFieldForm.vue # PDF 选择、拖拽、替换和移除控件 │ ├── ResourceRecordPage.vue # 保存编排、预览与失败回滚 │ ├── ResourceDetailContent.vue # 隐藏原始附件元数据对象 │ └── use-contract-attachment.ts # 附件页面状态与上传缓存 └── docs/ ├── 项目文档_配送合同附件上传_v1.0.md └── 操作日志_配送合同附件上传_20260812.md ``` ## 3. 核心设计 ### 3.1 存储与数据库 - 不新增表和字段,不修改 `GasorderContract` 模型。 - 正式 URI 形如 `/uploads/contracts/YYYY/MM/DD/<随机标识>.pdf`。 - 临时 URI 形如 `/uploads/contracts/temp/<随机标识>.pdf`。 - 页面只显示固定名称“合同附件.pdf”,下载名称为 `{合同编号}_合同附件.pdf`。 - 文件输入框封装在独立 `ContractAttachmentField.vue` 中,不位于字段 `v-for` 的模板引用范围内;上传区域支持鼠标、Enter、空格和拖拽操作。 ### 3.2 保存流程 1. 用户选择或拖拽 PDF,前端校验类型与大小,不立即上传。 2. 点击保存后调用合同专用上传接口。 3. 服务端验证扩展名、声明 MIME、真实 MIME、PDF 文件头、结束标记和大小,将文件写入临时目录,并返回绑定收据及清理凭证。 4. 合同创建或草稿更新接口验证收据的 HMAC、有效期和操作人,将临时文件移动至正式目录,再写入 `file_uri`。 5. 数据库保存失败时,服务端删除已移动文件;前端同时用清理凭证尝试清理仍在临时目录中的文件。 ### 3.3 替换、移除与并发 - 仅草稿合同可通过普通更新接口修改附件。 - 详情接口返回不含 URI 的 `attachment` 元数据,包括存在性、可用性、是否需要重新上传及不透明版本标识。 - 替换或移除必须提交页面加载时的版本标识;后端同时使用旧 `file_uri` 作为更新条件。 - 版本冲突会拒绝覆盖,并删除本次新绑定文件。 - 保存成功后删除旧文件;删除在当前请求内最多重试三次,最终失败写日志并由运维人工处理。 - 不创建定时扫描任务,不查询合同表清理未引用文件。 ### 3.4 权限与安全 - 所有专用接口位于 `/heqi/platform/v1/gasorder_contract` 下,复用 JWT 和 `gasorder_contract` 菜单权限。 - 下载接口只接收合同标识,并从合同记录读取 URI,不接受任意路径。 - 路径解析验证专用前缀、规范化相对路径和根目录边界。 - 合同启用前同时验证 URI 属于受控目录且磁盘 PDF 存在。 - 预览响应设置 `application/pdf`、`nosniff`、`sandbox` 和私有禁缓存响应头。 - 历史外部地址保留在数据库中,但不允许预览,也不能满足启用校验。 ## 4. API 说明 ### 4.1 上传临时合同附件 `POST /heqi/platform/v1/gasorder_contract/attachment/upload` - 请求:`multipart/form-data`,文件字段名为 `file`。 - 响应:`receipt`、`cleanup_token`、`display_name`、`size`。 ### 4.2 清理临时合同附件 `POST /heqi/platform/v1/gasorder_contract/attachment/cleanup` ```json { "cleanup_token": "签名清理凭证" } ``` 清理接口只能删除当前操作人上传且仍位于临时目录的文件。 ### 4.3 预览合同附件 `GET /heqi/platform/v1/gasorder_contract/:identity/attachment` 按合同标识鉴权返回 PDF,不返回内部 URI。 ### 4.4 合同创建与更新扩展字段 - 创建或替换:`attachment_receipt`。 - 替换或移除:`attachment_version`。 - 移除:`remove_attachment: true`。 - 原 `file_uri` 字段仍被后端兼容,但新版管理后台不再展示或提交裸 URI。 ## 5. 维护指南 - 增加新的合同附件格式时,必须同步修改前后端校验、下载响应类型和测试,不能只调整文件选择器。 - 更换对象存储时,应保持受控 URI 语义和合同标识下载接口不变,在后端存储映射层替换实现。 - 排查孤儿文件时,根据 `contract_attachment` 应用日志人工核对;当前设计明确不运行自动扫描任务。 - 调整附件权限时,应扩展平台菜单授权模型,不应新增公开静态目录。 ## 6. 变更记录 ### v1.0(2026-08-12) - 新增单 PDF 合同附件上传和受控预览; - 新增失败清理、草稿替换/移除和并发保护; - 新增合同启用附件完整性校验; - 保持数据库结构及通用上传接口不变; - 已知限制:不保留原始文件名,不保留历史版本,不自动扫描孤儿文件。 - 修复循环内模板引用被解析为元素数组导致文件选择器无法打开的问题,并新增键盘可访问性与专项组件契约检查。