Hcp Object Storage
ODTS-58: HCP 对象存储 / S3——合同、报表和附件的持久化底座
目标读者:想理解 ODTS 的合同 PDF、报表文件、上传附件等二进制数据如何存储和访问的 BA/PM
相关文档:ODTS-55 (Doc Agent), ODTS-31 (合同运营), ODTS-17 (交易确认)
概述
HCP (Hitachi Content Platform) 是 ODTS 使用的对象存储系统,用于存储所有二进制文件——合同 PDF、报表输出、上传附件、证书文件等。
ODTS 的设计模式是:数据库存元数据,文件本体存 HCP。这意味着系统的数据库中只有 URL/路径引用,不存文件二进制内容。
HCP 本质
HCP 实现了 AWS S3 兼容 API。ODTS 通过 AWS Java SDK (com.amazonaws.services.s3) 的 AmazonS3Client 访问 HCP:
// HCPClientUtil.java — HCP 连接初始化
AmazonS3ClientBuilder.standard()
.withClientConfiguration(clientConfig)
.withEndpointConfiguration(new EndpointConfiguration(endpoint, ""))
.withCredentials(new AWSStaticCredentialsProvider(
new BasicAWSCredentials(accessKey, secretKey)))
.build();
连接配置
| 配置项 | 来源 | 说明 |
|---|---|---|
endPoint | ConfigProp | HCP 服务端地址(HTTP) |
accessKey | ConfigProp | Access Key |
secretKey | ConfigProp | Secret Key |
nameSpace | ConfigProp | 默认 Bucket(namespace) |
ofanameSpace | ConfigProp | OFA 专用 Bucket |
协议:HTTP(不是 HTTPS),签名算法:S3SignerType(S3 v2 签名)。
两个 Bucket
ODTS 配置了两个 HCP namespace:
nameSpace(主桶):交易确认书、监管报送文件、上传附件、证书文件等ofanameSpace(OFA 桶):OFA(中金运营)相关的报表附件,通过独立的getOfaS3Client()访问
数据模型
HCP 的存储路径按业务目录组织,使用 {bucket}/{prefix}/{category}/{date}/{detail} 模式:
主桶:
{nameSpace}/confirm/SNB202507001.pdf ← 交易确认书
{nameSpace}/report/SAC_20250716.xml ← SAC 报送文件
{nameSpace}/upload/CLT0001_CSA_V2.pdf ← 客户上传的 CSA
{nameSpace}/cert/import/SNB2025070001.pfx ← 证书导入文件
{nameSpace}/data/godtsUatFile/geminiUpload/20250716/file.xlsx ← Gemini 上传文件
OFA 桶:
{ofaNameSpace}/.../option-attachment-20250716-010101-eds.csv ← 期权附件清单
{ofaNameSpace}/.../confirm-merged.pdf ← 合并确认书
OFA 报送路径规范
期权附件向 OFA 监管报送时遵循固定路径格式,在 UploadOptionAtts2S3Action 中构建:
CSV 路径:TEST/cicc/gm/ofa/data/regulator/interotc/{tradeDate}/{deskKey}/
示例:TEST/cicc/gm/ofa/data/regulator/interotc/2025-07-16/eds/
文件名:option-attachment-{yyyymmdd}-{deskId}-{deskKey}.csv
PDF 路径:TEST/cicc/gm/ofa/data/attachment/option/{tradeDate}/{deskId}/
示例:TEST/cicc/gm/ofa/data/attachment/option/2025-07-16/010101/
文件名:{contractId}-{confirmNo}.{ext}
deskId → deskKey 映射(DESKMAP):
| deskId(柜台代码) | deskKey(报送渠道) |
|---|---|
| 010101 | eds |
| 010201, 010202, 010301, 010401, 010601, 010701, 010801 | pb |
| 030101, 030102 | wsc |
HCPUtil —— 操作封装
HCPUtil 提供所有 HCP 操作的单例封装:
上传
| 方法 | 输入 | 用途 |
|---|---|---|
uploadFile(directory, uploadFilePath) | 目录 + 本地文件路径 | 从本地文件上传 |
uploadFile(file, path, fileName) | File + 路径 + 文件名 | 直接传 File 对象 |
uploadFileStream(key, inputStream) | S3 key + InputStream | 从流上传(通用方式) |
uploadFileDocStream(key, doc) | S3 key + XWPFDocument | 直接传 Word 文档对象 |
uploadFilePbConf(to, atomBean, doc) | 路径 + 合同信息 + Word 文档 | 交易确认书专用上传 |
uploadOfaFileStream(key, inputStream) | S3 key + InputStream | 上传到 OFA 桶 |
下载
| 方法 | 输入 | 输出 |
|---|---|---|
downloadFile(key, localFile) | S3 key + 本地路径 | 下载到本地文件 |
downloadFileByte(key) | S3 key | byte[] |
downloadFileToLocal(s3FilePath, localPath) | S3 key + 本地路径 | boolean |
管理和查询
| 方法 | 说明 |
|---|---|
deleteByObject(filePath) | 删除单个文件 |
deleteByFoldername(folderPath) | 删除目录下所有文件(逐个删除) |
copyObject(sourceKey, destKey) | 同一 bucket 内复制 |
rename(key, newKey) | 重命名(copy + delete) |
doesObjectExist(filePath) | 检查文件是否存在 |
listFiles(key) | 列出目录下文件列表 |
listObjectsV2(prefix) | 按前缀列出对象(含 S3ObjectSummary) |
getFileByFolderName(folderPath) | 获取目录下所有文件 key |
getFileNamesByFolderPath(folderPath) | 获取目录下文件名列表(不含路径) |
FileInfoHCP —— 文件迁移记录
FileInfoHCP.java
contractFileId — 合同文件 ID
srcKey — 源路径 (HCP)
tarKey — 目标路径 (HCP)
memo — 备注
用于记录文件从一个 HCP 路径迁移到另一个路径的信息(如 Doc Agent 转换后存储)。
S3OperationController —— REST API
系统通过 S3OperationController 将 HCP 操作暴露为 HTTP 接口:
| 路由 | 方法 | 说明 |
|---|---|---|
GET /s3/listObjectsV2 | listObjectsV2(prefix) | 按前缀列出文件 |
GET /s3/object | getObject(key) | 下载文件(二进制流) |
GET /s3/deleteByKey | deleteByKey(key) | 删除文件 |
GET /s3/saveReportToDisk | saveReportToDisk(date, desk, type) | 保存 EOD 报表到 HCP |
GET /s3/saveReportToDiskV2 | saveReportToDiskV2(date, desk, type) | V2 版本 |
GET /s3/saveStandardReportToDisk | saveStandardReportToDisk(date, desk, type) | 标准报表 |
GET /s3/downloadFileToLocal | downloadFileToLocal(s3Path, localPath) | 下载到本地 |
POST /s3/geminiUploadFile | geminiUploadFile(MultipartFile) | Gemini 上传文件 |
文件下载流程(通过 S3OperationController)
用户请求下载 → GET /s3/object?key=confirm/SNB202507001.pdf
│
▼
HCPUtil.downloadFileByte(key) → byte[]
│
▼
HTTP Response:
Content-Type: application/octet-stream
Content-Disposition: attachment;filename=SNB202507001.pdf
Content-Length: {bytes.length}
│
▼
浏览器下载文件
Gemini 上传流程
外部系统上传文件 → POST /s3/geminiUploadFile (MultipartFile)
│
▼
S3 路径: /data/godtsUatFile/geminiUpload/{yyyyMMdd}/{originalFileName}
│
▼
HCPUtil.uploadFileStream(key, byteArrayInputStream)
│
▼
返回: { "key": "...", "fileName": "..." }
文件类型清单
| 类别(category) | 文件内容 | 上传者 | 访问者 |
|---|---|---|---|
confirm | 交易确认书 PDF/Word | Doc Agent | 运营、客户 |
upload | 客户/运营上传的合同扫描件 | 运营 | 运营、法务 |
report | 监管报送文件/报表 | eds-web-app | 运营 |
cert | 数字证书文件 | 运营 | Cert App |
margin | 保证金对账单 | eds-web-app | 运营、客户 |
template | 上传的模板文件 | 管理员 | Doc Agent |
元数据存储
ODTS 使用 java.util.HashMap<String, String> 存储文件的元数据:
| Key | Value 示例 | 说明 |
|---|---|---|
fileType | CONFIRMATION | 文件类型 |
hcpUrl | /hcp/ns/odts/confirm/SNB202507001.pdf | HCP 存储路径 |
uploadTime | 2025-07-16T10:30:00 | 上传时间 |
operator | operator01 | 上传人 |
fileSize | 245678 | 文件大小(字节) |
checksum | sha256:abc123... | 文件校验和 |
这种”Map 即 schema”的模式在 ODTS 中很常见——灵活但无编译时类型检查。
为什么用 HCP 而不是数据库 BLOB?
| 维度 | 数据库 BLOB | HCP 对象存储 |
|---|---|---|
| 存储成本 | 高(数据库存储贵) | 低(对象存储≈1/10 成本) |
| 读写性能 | 大文件影响数据库查询性能 | 独立扩展,不影响数据库 |
| 备份恢复 | 数据库备份包含大量二进制,又大又慢 | 独立的备份策略 |
| 版本控制 | 需要自己实现 | HCP 原生支持 |
| CDN 分发 | 不支持 | 可通过网关做 CDN |
| API 兼容 | JDBC | S3 兼容 API |
代码架构评价
合理的设计
- S3 兼容 API:HCP 暴露标准 S3 接口,如果未来要迁移到 AWS S3 或 MinIO,只需要改 endpoint
- 单例模式:
HCPUtil和HCPClientUtil使用单例,避免重复创建 S3 客户端 - HTTP 协议:内网环境使用 HTTP 减少 TLS 开销,合理
- 操作覆盖完整:CRUD + 列表 + 复制 + 重命名 + 存在性检查
值得关注的问题
- S3 v2 签名:
S3SignerType是旧的 v2 签名算法,部分新版本的 HCP 或 S3 兼容服务可能不再支持 - HTTP 明文:内网 HTTP 本身问题不大,但如果数据需要跨境传输或跨机房,缺乏加密
- 单点失败:HCP 客户端是进程内单例,重启前不会重连;如果 HCP 服务端切换 IP,需要重启应用
- 大文件下载:
downloadFileByte将整个文件读入内存 byte[],大文件可能导致 OOM - 没有分片上传:
uploadFileStream没有使用 S3 的分片上传(multipart upload),大文件传输效率低 - 文件夹删除逐条执行:
deleteByFoldername逐个调用deleteByObject删除文件,目录下有大量文件时性能很差
这些问题对业务的真实代价
| 技术问题 | 触发场景 | 业务后果 | 量级 |
|---|---|---|---|
| 单点失败(#3) | HCP 服务端切换 IP / 重启 | ODTS 取不到任何合同 PDF、报表、附件;且要恢复得重启整个 eds-web-app 应用(连带影响所有交易功能,不仅是文件) | 🔴 全局中断 |
| HTTP 明文(#2) | 合同/证书/CSA 文件在内网传输 | 文件内容是客户交易确认书、数字证书私钥引用——明文传输 + 47 文档里那些”已提交到 Git 的密钥”,组合风险:内部抓包即可拿到敏感文件 | 🟠 合规风险 |
| 大文件 OOM(#4) | 运营导出合并确认书 / 大报表 | 单次下载触发 JVM 内存溢出 → 该应用实例崩溃 → 该节点交易功能停摆 | 🟠 稳定性 |
| 无分片上传(#5) | 上传大批量期权附件(OFA 报送) | 传输慢、超时重试,报送窗口内可能传不完 → 监管报送延迟(见 38) | 🟡 效率 |
| v2 签名(#1) | 未来 HCP 升级 / 迁移 MinIO | 签名不兼容 → 全量文件读写失败 → 必须改代码重新发版才能恢复 | 🟠 迁移风险 |
PM/BA 启示:
- HCP 不是”一个存储”那么简单。它是合同法律效力(确认书)、监管证据(报送 XML)、运营凭证(附件)的唯一持久化底座(见 52 的”HCP 挂掉”爆炸半径)。它的可用性要直接进入 ODTS 的 SLA,而不是当成”基础设施自己管”。
- “数据库存元数据、文件存 HCP”这个设计本身是对的(成本低、不拖慢 DB),但它制造了一个隐性的强耦合:HCP 不可达 = 文件全部不可达,而元数据还在 Oracle 里——运营会看到”记录存在但文件打不开”的怪异状态,排查成本极高。
- 与 47 文档同源:内网 ≠ 安全。HCP 走 HTTP 明文 + 密钥曾经明文入库,合在一起意味着”能进内网的人”理论上能拿到客户合同原文。这是审计口径下的高风险组合。
关键文件
| 组件 | 路径 |
|---|---|
| HCP 操作封装 | eds-web-app/src/.../utils/HCPUtil.java |
| HCP 客户端 | eds-utility/src/.../utils/HCPClientUtil.java |
| REST 控制器 | eds-web-app/src/.../edsBoot/controller/S3OperationController.java |
| 文件迁移模型 | eds-web-app/src/.../action/dynamic/FileInfoHCP.java |
| 期权附件上传 | eds-web-app/src/.../action/dynamic/UploadOptionAtts2S3Action.java |
| 交易确认书上传 | eds-web-app/src/.../action/dynamic/UploadOptionConfirmationAttsCSV2S3Action.java |
| OFA 报送保存 | eds-web-app/src/.../action/dynamic/SaveFiDttNReportToS3Action.java |
| 保证金文件类型 | eds-web-app/src/.../edsBoot/model/margin/enums/MarginS3FileType.java |
| S3 文件摘要 | eds-web-app/src/.../edsBoot/service/margin/client/FileS3SummaryVo.java |
| HCP 测试 | eds-web-app/test/.../utils/HCPUtilTest.java |
| SAC 报送 HCP | sac-report/src/.../utils/HCPUtil.java |
| 参数选项 S3 JS | oss-image/server/s3.js |