文档类型:目标技术设计。产品语义见 requirements.md,当前代码差异见 implementation-status.md。
Task Module
├─ DataDownloadTask / Item / FileAttempt
├─ TaskDocument(用户访问入口)
└─ Queue / Cancel / Retry / Aggregate
FileRequestService
├─ 规范化 RequestKey
├─ DocumentID
├─ 分布式请求租约与续租
└─ 相同请求合并
DocumentService
├─ Document Catalog
├─ PostgreSQL 当前结构化正文
├─ MinIO 原始对象及派生 Artifact
├─ Mutable / Frozen / Dynamic
└─ Renderer / Package 输入
Task Module 管理“谁请求过、执行结果怎样”;DocumentService 管理“当前有哪些可复用数据”。DocumentService 不以 taskID 作为内容身份。
DataDownloadTask:owner、条件、总状态和汇总计数;DataDownloadItem:一个可独立执行和重试的 DocumentUnit;DataDownloadTaskDocument:Task 到 Document 的授权入口和逻辑路径;DataDownloadFileAttempt:FaultRecord 某个物理文件在本任务中的执行历史;DataDownloadPackageJob:异步 ZIP 生命周期。DataDownloadDocument:稳定逻辑数据单元及当前状态;DataDownloadFaultRecordEntry:FaultRecord 文档下稳定文件身份和 current object 指针;DataDownloadRenderSlot:文档格式到当前派生 Artifact 的指针;DataDownloadArtifact:不可变派生对象。MongoDB collection 必须独立保存 Task、Item、TaskDocument、Document、Entry、Attempt、RenderSlot、Artifact 和 PackageJob,避免无限数组嵌入主任务。
RequestKey 只包含影响正文身份的字段,不包含 taskID、owner、描述、创建时间或输出文件名。
当前规则:
| Kind | RequestKey 字段 |
|---|---|
| PowerFlow | kind、content_version、SiteSN、Date、Timezone |
| MQTT | kind、content_version、SiteSN、Date、SubType、Timezone |
| EventAlarm | kind、content_version、SiteSN、Date、Timezone |
| AlarmSnapshot | kind、content_version、SiteSN、Date、Timezone |
| EMS | kind、content_version、SiteSN、Date、SubType |
| FaultRecord | kind、content_version、DeviceSN、DevType、Date、Timezone |
| UpdateLog | kind、content_version、SiteSN |
字段值需要规范化并安全转义;每个 kind 独立维护 content_version。只有 query/parser/schema 的内容语义变化才提升版本,RendererVersion 不进入 RequestKey。
DocumentID = SHA-256(RequestKey)
任务内部 ItemKey 仅用于幂等规划,不能代替 RequestKey。
文档状态为 Mutable、Frozen、Dynamic,另有 Completeness=complete|partial。
日期 D 的 Frozen 判定:
当前站点本地时间 >= D+2 00:00
AND 本轮查询发生在该边界之后
AND 完整上游查询成功
AND 所有要求内容均成功发布
AND 无待重试失败
边界前生成的 Mutable 文档不能由定时器直接翻转 Frozen。空结果在完整查询成功后也可以 Frozen。
结构化数据只保存当前正文,不保存历史快照。data_download_content_state 保存 DocumentID、Kind、Generation、SchemaVersion、RowCount、Checksum、SourceFetchedAt。
正文表按类型建模:PowerFlow 点数据、MQTT entries、EventAlarm entries、UpdateLog entries。更新使用事务内全量替换并递增 Generation;大批量写入可从 GORM batch 演进到 pgx CopyFrom。
MongoDB 保存任务状态、文档目录、对象指针、文件 Attempt、派生缓存 Slot 和 PackageJob。Document 的 Generation 是 PostgreSQL 权威代数的镜像,不应成为结构化正文的独立事实来源。
关键唯一索引:
taskId + itemKey;taskId + itemKey + documentId;requestKey;taskId + itemId + sourceFileId;documentId + sourceFileId;documentId + format。原始二进制、FaultRecord 文件、RenderArtifact 和 Package ZIP 使用不同前缀。对象键不得通过 HTTP/MCP JSON 暴露。
对象先以不可变 key 上传,校验成功后再切换 Mongo current pointer。指针切换失败时删除新孤儿对象;删除失败则需要可重试的 orphan/retiring 机制。
规划阶段从站点信息取得 aGate SN,从站点基本信息的 FhpSn 取得 aPower SN,按每个 DeviceSN + DevType + DataDate 创建 Item。当前只规划 DevType 0/1;规划阶段不查询 faultRecordingLst,因此执行 Item 时的第一项上游录波请求是 getWaveRecordingCatalog。保留的 SN 映射如下:
| DevType | SN 来源 |
|---|---|
| 0 aPower | ApowerId |
| 1 aGate | GatewayId |
| 2 MPPT | 暂不处理;历史记录中的字段为 ApowerId |
| 3 aHub | 暂不处理;历史记录中的字段为 PowerlinkId |
未知 DevType 返回明确错误,未来通过策略表扩展。
执行流程:
| 步骤 | 方法与上游路径 | 主要参数与判定 |
|---|---|---|
| 可召唤目录 | GET /hes-gateway/manage/getWaveRecordingCatalog |
deviceSerial、callType;按 failureTime 过滤日期 |
| 已召唤列表 | GET /hes-gateway/manage/faultRecordingLst |
deviseSerialNumber、earliestStartTime、earliestEndTime;整个列表为空表示尚无已召唤记录 |
| 子记录校验 | GET /hes-gateway/manage/lstSubFaultWave |
faultId;数量须等于父记录的 faultRecordsCount,再比较子项 failureTime 与故障码 |
| 发起召唤 | POST /hes-gateway/manage/callFaultRecording |
deviceId、逗号分隔的 index、callType、triggerType=2 |
| 召唤状态 | GET /hes-gateway/manage/lstFaultWaveCall |
pageNum、pageSize;初次按设备、类型、数量、TriggerType、cellTime ±1 分钟 定位 ID,此后按 ID 查找 |
| 原始文件 | GET /hes-gateway/manage/exportSubFaultWave |
faultId、id(子记录 ID) |
获取 Document 请求租约
-> getWaveRecordingCatalog 按 failureTime 选目录条目
-> faultRecordingLst + lstSubFaultWave 校验已召唤内容与数量
-> 对缺失条目执行 callFaultRecording
-> lstFaultWaveCall 按设备、DevType、数量、TriggerType、cellTime 定位任务 ID
-> 每轮按 ID 查询一次状态,约 60 秒后延迟重入队;总计最多 2 小时,超时保存最后状态
-> 成功后 faultRecordingLst 按 ID 定位,lstSubFaultWave 再校验数量
-> 对未成功的 sourceFileID 分别下载
-> 上传不可变对象
-> 在 Document content lock 内切换 Entry current pointer
-> 记录本 Task 的 FileAttempt
-> 聚合 Entry 和 Attempt
-> 写入 Document completeness/state
-> 创建 TaskDocument 入口
sourceFileID 当前由 faultID:subRecordID 构成。文件名仅展示,允许重复。新查询没有返回旧 Entry 时不删除旧 Entry。
本次召唤流程使用 FaultRecord content_version=v2,以免旧流程产生的 Frozen 文档被新请求直接复用。
faultRecordingLst 成功返回 result: [] 时,进入缺失项召唤分支。lstSubFaultWave 对某条记录返回空列表时,仍需与其 faultRecordsCount 比较;若数量不符,该记录无效。接口错误应作为错误处理,不应推断成无记录。
轮询期间持久化召唤发起时间、数量、任务 ID 和最后状态,并用 Mongo 中按 DocumentID 保存的召唤占用记录约束同一文档只有一个 Item 发起召唤。每轮查询后将 Item 恢复为 pending,延迟约 60 秒重入队,立即释放 worker;重入队时重新取得短时 FileRequest 租约。召唤成功、远端明确失败或 2 小时超时后释放占用。任务取消或延迟入队失败时,如果远端召唤可能仍在运行,占用保留到终态或到期,以免其他 Item 重复召唤。占用记录在原始 2 小时窗口后附加 10 分钟到期,避免 worker 崩溃后无限占用。轮询间隔允许有队列调度误差。
失败刷新不能切换 Entry;新任务 Attempt 仍失败。重试读取该 Task 的 Attempt,仅跳过已经 success 且 Entry 仍存在的文件。
相同 DocumentID 使用 Redis 请求租约合并:
Document 正文、Entry current pointer、Artifact 读取和物理删除使用同一 Document content 协调边界。目标方案应为 writer-preference 读写锁;当前实现仍使用排他 Redis lock,详见实现状态。
写入顺序应避免 PostgreSQL、Mongo Document 和 RenderSlot 三者出现可命中的旧组合:
取得写租约
-> 使旧 RenderSlot 不可命中
-> PostgreSQL 事务替换正文并递增 Generation
-> 回填 Mongo Document 镜像
-> 释放写租约
Renderer 按 DocumentID + Format + ContentGeneration + RendererVersion 生成不可变 Artifact。发布必须使用 generation/token CAS,缓存命中必须校验 PostgreSQL 权威 Generation。
Item 可为 pending、running、success、partial_success、failed、skipped、cancelled。FaultRecord 文件级结果先聚合成 Item,再聚合 Task。
普通任务入口:
GET /dataDownload/tasks/:idGET /dataDownload/tasks/:id/documents/:documentId/fileGET /dataDownload/tasks/:id/files/:entryId这些接口同时校验 owner/admin 和 TaskDocument 关联。
管理员目录入口:
GET /dataDownload/documentsGET /dataDownload/documents/:documentId/fileGET /dataDownload/documents/:documentId/filesGET /dataDownload/documents/:documentId/files/:entryIdDELETE /dataDownload/documents/:documentId管理员强制删除需要先获取 request/content lock,删除正文、原始对象、Entry、Artifact 和目录元数据。
PackageJob 在后台解析 TaskDocument:结构化文档先渲染,普通二进制读取 current object,FaultRecord 枚举各 Entry。多任务时以 TaskID 为顶级目录。
存在失败 FileAttempt 时,把成功文件和 DataDownloadReport.json 一起打包。输入文件读取失败或 Renderer 失败会使整个 Job 失败。
ZIP 保存为不可变对象,成功后设置 CompletedAt、ExpiresAt=+7d 和后续 PurgeAt。下载期间与清理使用相同锁。
MCP owner 固定为 USER_AI。逻辑查询以 DocumentUnit 为输入,返回:
missing | fetching | partial | available | last_fetch_failed
结果至少包含 State、Completeness、Empty、SourceFetchedAt、FileCount、FailedFileCount。缺失时再检查 USER_AI 自身活跃任务覆盖并创建任务。任务指纹/短期锁用于“检查后创建”的幂等,不能替代 Document RequestKey。