# DataDownload 待解决问题 > 建立日期:2026-09-19 > 状态:持续维护 > 文档导航:[DataDownload README](../data-download/README.md);目标需求见 [requirements.md](../data-download/requirements.md);技术设计见 [technical-design.md](../data-download/technical-design.md);当前实现见 [implementation-status.md](../data-download/implementation-status.md)。 > 范围:DataDownload 首版实现差异、可靠性风险,以及当前阻断全仓测试的既有源码问题。 ## 1. 使用规则 - 状态取值:`open`、`in_progress`、`blocked`、`resolved`、`wont_fix`。 - 优先级取值: - `P0`:可能造成数据错误、并发覆盖、返回旧内容或任务永久异常,发布前必须解决。 - `P1`:可能造成资源泄漏、内存故障、重要功能缺失或运维风险。 - `P2`:产品能力、展示或工程质量未完整对齐设计。 - `P3`:仓库既有问题,不直接影响 DataDownload 主链路,但会阻断全仓验证。 - 关闭问题时应填写解决提交、验证方式和关闭日期,不直接删除历史条目。 - 产品目标以 `requirements.md` 为准,技术约束以 `technical-design.md` 为准,当前事实以源码、测试和 `implementation-status.md` 为准。 ## 2. 汇总 | ID | 优先级 | 状态 | 问题 | |---|---|---|---| | DD-001 | P0 | open | Redis 锁没有续租和 fencing token(观测优先:第 1 层已落地,第 2/3 层待触发) | | DD-002 | P0 | resolved | PostgreSQL、Mongo Document、RenderSlot 提交不具备崩溃一致性(方向 A:先失效后提交;读侧竞态转 DD-008) | | DD-003 | P0 | resolved | FileRequestService 未按 RequestID/延迟等待模型实现 | | DD-004 | P0 | resolved | plan handler 重试不幂等 | | DD-005 | P0 | resolved | RequestKey 缺少影响正文身份的字段 | | DD-006 | P0 | resolved | FaultRecord 文档粒度和存储模型错误 | | DD-007 | P1 | open | Artifact 发布缺少 generation/token CAS 校验 | | DD-008 | P1 | open | 缓存命中未校验 PostgreSQL 权威 Generation | | DD-009 | P1 | open | 二进制切换失败会产生孤儿对象 | | DD-010 | P1 | open | 大文件读取与 ZIP 构建存在内存放大 | | DD-011 | P1 | open | PostgreSQL migration 多实例并发不安全 | | DD-012 | P1 | open | 任务删除不是事务或原子状态变更 | | DD-013 | P1 | resolved | PackageJob PurgeAt 计算基准错误 | | DD-014 | P1 | open | MinIO Repository 没有传播 context | | DD-015 | P1 | open | 归档内部文件读取没有大小上限 | | DD-016 | P2 | open | PostgreSQL 强类型结构仍处于过渡态 | | DD-017 | P2 | open | Renderer 输出未完整对齐设计 | | DD-018 | P2 | open | 前端真实预览与归档浏览未实现 | | DD-019 | P2 | open | 前端批量操作和部分文件选择未实现 | | DD-020 | P2 | open | 任务、Package 和管理员页面过滤/权限展示不完整 | | DD-021 | P2 | open | MCP 能力和幂等创建未完整实现 | | DD-022 | P2 | open | MongoDB 索引与设计索引表不完全一致 | | DD-023 | P2 | open | 自动化测试覆盖不足 | | REPO-001~007 | P3 | open | 仓库既有编译和旧测试签名问题 | ## 3. P0:数据正确性与并发问题 ### DD-001 Redis 锁没有续租和 fencing token - 状态:`open`(当前采用「观测优先」策略:先补充可观测性,待真实样本再决定 CAS/TTL 方案) - 位置:`service/cache/redisx/lockx.go`、`service/services/dataDownloadService.go`、`dataDownloadBinaryService.go` - 当前行为:FileRequest 租约和流式读取锁已经支持 token 校验续租;普通 content 锁和 RenderArtifact 发布仍没有完整 fencing/CAS。 - 影响:大型 Renderer 或正文提交超过 content lock TTL 后仍有租约过期窗口;Artifact 发布也仍可能被旧执行者覆盖。 - 处理建议:把 content/render 锁统一为可续租租约,并在 Artifact/正文发布处加入 generation/token CAS。 - 验收条件:覆盖锁续租、租约丢失、旧持有者晚提交、worker 崩溃后恢复的并发测试。 #### 决策:观测优先,暂不实施 CAS 该问题「理论存在、实际频率未知」,真实行数量级与 PG/MinIO 实际延迟均为估算。generation CAS 改造牵连 DD-002/007/008,投入较大。因此先补充观测日志,让问题从「悄悄写坏」变为「事后可判定、可定位」,攒到真实的耗时分布与 owner 丢失样本后,再决定是上 CAS 还是仅调 TTL / 改批量写入。本条在样本充分前保持 `open`,不因加了日志而视为已修复。 #### 观测手段进度 - **第 1 层(已落地,2026-09-20)**:`service/cache/redisx/lockx.go` 的 `RedisLock` 内建观测,一处覆盖全部锁(content / package / request lease,`cache.RedisLock` 为其类型别名,调用方透明): - 新增 `acquired`(加锁时刻,单调计时)、`label`(从 key 前两段推断,如 `data-download:content`)。 - `SafeRelease` 分三档记录:正常释放但持锁 > TTL×70% 记 `Warn`;释放时发现 `not owner`(锁被抢走、可能已并发覆盖)记 `Error`(关键铁证,带 label/key/token/held/ttl);其他释放失败记 `Error`。 - 引入哨兵错误 `errLockNotOwner`,统一 `Release`/`LockRelease`/`Extend`/`SetIfOwner` 的 not-owner 返回值,供 `errors.Is` 精确判定。 - 验证:`go build ./cache/...`、`go test ./services ./cache/redisx ./database` 通过。 - **第 2 层(待补,出问题后再加)**:写入临界区分段耗时 + 数据规模。 - 位置:`ExecuteStructuredDataDownloadItem` / `ExecuteBinaryDataDownloadItem` 拿锁之后。 - 记录字段:`documentId`、`taskId`、`kind`、`rowCount`(结构化)/`objectBytes`(二进制)、分段耗时(`replaceRows`/`minioPut`/`mongoWrite`)、`total`、`ttl`。 - 日志策略(定稿):正常路径打 `Debug`(生产默认 Warn 级别,不输出,零成本;需要时可临时调级看全量);`total ≥ TTL×50%` 打 `Warn` 带完整分段。默认零噪音,与第 1 层职责不重叠(第 1 层管「有没有出事/接近出事」,第 2 层管「出事时慢在哪一步」)。 - **第 3 层(待补,视第 2 层结果再加)**:`ReplaceDataDownloadRows` 内部 `pg_advisory_xact_lock` 获取耗时。 - 目的:区分「逐行 insert 慢」与「advisory 锁等待慢(写-写争用,正是 DD-001 的并发场景)」。 - 触发条件:第 2 层日志显示 `replaceRows` 大但 `rowCount` 不高时,再补此层定位。 - 记录:advisory 锁获取耗时,> 若干秒打 `Warn`。 ### DD-002 PostgreSQL、Mongo Document、RenderSlot 崩溃一致性 - 状态:`resolved`(方向 A,2026-09-20) - 位置:`service/services/dataDownloadService.go`(`ExecuteStructuredDataDownloadItem` 提交段) - 原行为:先提交 PostgreSQL,再更新 Mongo Document,最后断开 RenderSlot。「PG 已提到 N、Mongo Document 仍为 N-1」的窗口内,`currentArtifact` 因 `slot.ContentGeneration == document.ContentGeneration`(都为 N-1)判定命中,静默返回旧代缓存且不自愈。 - 解决方式(方向 A:先失效、后提权威、再回填):将 `DetachDataDownloadRenderSlots` 从「PG + Upsert 之后」提前到 `ReplaceDataDownloadRows` **之前**。断开 RenderSlot 后 `currentArtifact` 会因 `slot.ArtifactID==nil` 判定 miss(`GetDataDownloadRenderSlot` 无 slot 时返回 `ErrNoDocuments`,调用方仅在 `err==nil` 时命中,其余一律重渲染),强制后续读者从 PG 当前 generation 重新渲染。 - 逐窗口验证(初始三者均为 N-1,目标 N): - Detach 后 / PG 前崩溃:slot 断开、PG=N-1 → 读 miss 重渲染读 PG(N-1),正确。 - PG 后 / Upsert Document 前崩溃(原危险窗口):slot 断开、PG=N、Doc=N-1 → 读 miss 重渲染读 PG(**N**),正确,不再命中旧代缓存。 - 全部成功:slot 断开、PG=N、Doc=N → 读 miss 重渲染读 PG(N),正确。 - 反向风险:Detach 提前后若 PG 提交失败返回,仅表现为缓存被清、下次读重渲染同一代内容,无数据错误。 - 影响范围:仅结构化路径(PowerFlow/MqttLog/EventAlarm/UpdateLog)。二进制路径(AlarmSnapshot/EmsLog/FaultRecord)走 `BinaryObjectKey` 直读、无 RenderSlot,不涉及本窗口。 - 验证:`go build ./services/...`、`go vet ./services/`、`go test ./services ./repositories/pgsql ./database` 通过;崩溃注入的端到端恢复测试仍归 DD-023。 - 残留(转由其他条目跟踪): - **DD-008**:非崩溃的正常并发下,读取仍只比较 Mongo 侧 generation、未回读 PG 权威 generation。方向 A 消除了「崩溃后命中旧缓存」,但「读取信任 Mongo 副本而非 PG 权威」的读侧竞态需由 DD-008(缓存命中回读 PG generation)闭合。 - 说明:本系统尚未上线、数据库无历史 DataDownload 数据,不存在旧提交顺序遗留的不一致文档,因此无需存量对账/修复任务。方向 A 自首条写入起即生效。 ### DD-003 FileRequestService 请求协调缺失 - 状态:`resolved` - 解决日期:2026-09-19 - 解决方式:新增独立 `data-download:request:{DocumentID}` 租约,RequestID 使用租约 token;等待者携带 `AwaitRequestID` 按 15/30/60 秒退避重新入队,保持 pending,不占用 worker。租约自动续期,提交前校验 owner token,完成标记通过 Lua 在仍持有租约时原子写入。Mutable 同轮等待者复用对应 RequestID 的结果;Dynamic 完成等待后重新竞争下一轮租约。 - 验证:DataDownload 后端相关包编译和单元测试通过;跨进程 Redis/Asynq 集成场景仍归 DD-023。 ### DD-004 plan handler 重试不幂等 - 状态:`resolved` - 位置:`service/task/queue/dataDownloadQueue/dataDownloadQueue.go:132-195` - 解决日期:2026-09-19 - 解决方式:item 写入改为基于 `(taskId,itemKey)` 的无序 BulkWrite upsert,并回填已存在 item 的 `_id`;稳定 Asynq TaskID 冲突按“已经调度”处理。plan 和 `site_validation` 重规划均可重复执行。 - 验证:相关包编译通过;MongoDB/Asynq 故障注入测试仍归 DD-023。 ### DD-005 RequestKey 维度不足 - 状态:`resolved` - 位置:`service/services/dataDownloadService.go:168-170` - 解决日期:2026-09-21 - 解决方式:新增独立的规范化 RequestKey 构造器,按数据类型选择实际影响正文身份的字段;`UpdateLog` 不带日期和时区,MQTT/EMS 按需包含 SubType,按本地日期查询或解析的类型包含实际时区,FaultRecord 使用 DeviceSN/DevType 设备级身份。每种类型使用独立 `content_version` 合并管理 query/parser/schema 语义版本,RendererVersion 不进入 RequestKey。 - 链路:DocumentID、Redis 请求租约和 Mongo Document 均使用同一个规范化 RequestKey;任务内部 `ItemKey` 不再充当文档身份。 - 验证:增加所有数据类型的确定性、字段隔离、转义和非法身份单元测试;DataDownload 服务、队列及 Mongo/PostgreSQL Repository 相关测试通过。 ### DD-006 FaultRecord 模型错误 - 状态:`resolved` - 解决日期:2026-09-21 - 解决方式:规划阶段按设备拆分 `DeviceSN + DevType + DataDate` item;执行阶段建立 `DataDownloadFaultRecordEntry` 多文件目录与任务级 `DataDownloadFileAttempt`,每个录波文件独立上传、发布和重试,不再创建每日 ZIP。TaskDocument 路径包含设备和日期,任务详情返回文件目录及 Attempt,并提供校验 task owner/admin 的任务入口单文件下载接口。 - 新增决策:FaultRecord 同样执行 D+2,Frozen 作用于 `DeviceSN + DevType + DataDate` 文档单元;成功文件立即可用,文件级失败使 item/task 为 partial,旧当前文件在刷新失败时继续服务但不能计作新任务成功;上游后续未返回的历史文件继续保留;首版一键重试全部失败文件,不提供单文件人工重试。 - 访问语义:底层 Document/File 可跨任务复用,普通用户只能通过自己的 TaskDocument 入口访问;删除任务即失去入口但不删除共享文件。MCP 使用非管理员特殊用户 `USER_AI`,AI 以数据可用性为中心查询,AI任务仅管理员可见。 - 验收条件:支持一个日文档关联零到多个原始对象;空结果可固化;目录按设备和原始文件展示;保存文件级 Attempt;partial ZIP 包含成功文件及 `DataDownloadReport.json`;D+2 后完整查询且无失败条目才能 Frozen。 - 验证:FaultRecord 空结果和完整结果按 D+2 规则写入 Mutable/Frozen;失败文件不切换旧 Entry,成功文件立即可读;一键重试只会再次处理未成功文件;PackageJob 收集独立对象,并在存在失败时生成含任务汇总、设备、日期、失败阶段和原因的 `DataDownloadReport.json`。相关 Service、Queue、Controller、Route、Mongo/PostgreSQL 包测试通过。 ## 4. P1:可靠性、资源和运维问题 ### DD-007 Artifact 发布缺少 CAS - 当前行为:`PublishDataDownloadArtifact` 直接按 `(documentId,format)` upsert Slot。 - 处理建议:增加 RendererVersion、CacheKey、Slot.Generation、渲染 token 和 Artifact 状态;发布时原子校验 PG Generation 与 Slot generation。 ### DD-008 缓存命中未校验 PostgreSQL Generation - 当前行为:只比较 Mongo Document 和 Slot 的 Generation。 - 影响:DD-002 的失败窗口中可能继续返回旧 Artifact。 - 处理建议:在文档锁内读取 PG content_state,只有三者一致才允许命中。 ### DD-009 二进制上传孤儿对象 - 位置:`service/services/dataDownloadBinaryService.go:129-160` - 当前行为:上传成功后 Mongo Upsert 失败会直接返回。 - 处理建议:失败时尽力删除新对象;删除失败则登记 orphan/retiring 记录供清理任务重试。 ### DD-010 大对象内存放大 - 位置:Renderer、BinaryService、PackageService、Controller。 - 当前行为:HTTP 的 Package、二进制文档和缓存 RenderArtifact 已改为流式响应;Renderer 新生成内容、BinaryService 上游文件和 ZIP 构建仍使用 `[]byte`/`bytes.Buffer`。 - 影响:多站点、600 天或大录波任务可能导致 OOM。 - 处理建议:使用受控临时文件或流式管道;限制单条预览大小;MinIO 上传支持流和取消。 ### DD-011 migration 多实例并发不安全 - 位置:`service/database/data_download_schema.go` - 当前行为:自制 migration table,没有 migration advisory lock,也没有 goose/embed。 - 影响:两个实例首次启动可能同时判断版本缺失,其中一个因版本主键冲突启动失败。 - 处理建议:采用 goose/embed,或至少在迁移事务前获取固定 PostgreSQL advisory lock。 ### DD-012 任务删除非事务化 - 位置:`service/repositories/mongo/dataDownloadRepo.go` 的 `DeleteDataDownloadTask`。 - 当前行为:依次删除 TaskDocument、Item、Task。 - 影响:中途失败形成部分删除;状态检查与删除之间也存在竞态。 - 处理建议:Mongo transaction,或使用 deleting 状态和可重入清理流程;删除主记录时带终态条件。 ### DD-013 PackageJob PurgeAt 计算错误 - 状态:`resolved` - 位置:`service/services/dataDownloadCleanupService.go:27` - 解决日期:2026-09-19 - 解决方式:清理时统一写入 `PurgeAt = ExpiresAt + 30天`,不再以清理实际执行时间为基准;下载与清理使用同一个 Package 锁,避免流式响应期间删除对象。 ### DD-014 MinIO context 未传播 - 位置:`service/repositories/minio/dataDownloadRepo.go` 和现有 `utils/minio`。 - 当前行为:Repository 接收 context,但底层 helper 不使用它。 - 影响:用户取消、任务超时或服务关闭后,对象操作不能及时中断。 ### DD-015 归档读取缺少大小上限 - 位置:`service/services/dataDownloadArchiveService.go`。 - 当前行为:内部文件按 tar header.Size 读入内存。 - 影响:异常或恶意归档可能消耗大量内存。 - 处理建议:配置预览上限;超限只允许流式下载;验证 header.Size 和实际读取量。 ## 5. P2:功能和设计完整性问题 ### DD-016 PostgreSQL 结构仍是过渡态 - 强类型列已经加入,但仍保留早期 `payload jsonb`、通用 UpdateLog 表和冗余字段。 - UpdateLog device version 尚未展开为完整 Gate/子设备版本行。 - EventAlarm 的可解析开始/结束时间列不完整。 - 应制定新 migration,清理或明确保留兼容列,避免继续在同一个迁移版本追加语句。 ### DD-017 Renderer 未完整对齐 - PowerFlow HTML 当前输出 JSON `
`,未生成 ECharts。
- UpdateLog XLSX 当前是单列 payload,未生成 `SystemUpdate`、`aHubUpdate` 两个 Sheet。
- JSON 输出没有文档元信息和结构化 envelope。
- MQTT/EventAlarm 固定列已经实现,但仍需使用真实数据回归旧导出格式。
### DD-018 前端真实预览与归档浏览缺失
- 当前任务和管理员页面只有下载按钮。
- 尚无 html/csv/xls/xlsx/log/txt/json 真实预览。
- 后端已有 tar.gz 列表/内部读取 API,但前端没有目录浏览和内部文件预览。
### DD-019 批量操作和部分文件选择缺失
- 后端 Package API 可以接受多个 TaskID,但页面没有任务多选。
- 没有批量删除、目录树部分文件选择及权限不符项静默跳过。
- 没有“解析后一个实际文件直接下载,两个以上创建异步 ZIP”的选择逻辑。
### DD-020 页面过滤和权限展示不完整
- 任务创建时间过滤后端已支持,但前端未接入。
- 描述修改 API 已有,但前端未接入。
- 查看“所有任务”时,非 owner 仍能看到取消/重试/删除按钮,只在后端拒绝。
- Package 页面缺少状态和创建时间过滤。
- 管理员页面缺少 Kind、SN、日期、SubType、状态过滤和完整对象清单。
### DD-021 MCP 能力不完整
- 当前只有任务列表、创建任务和文档分页列表。
- 缺少文档过滤/详情/读取、tar.gz 浏览、存在性判断、AI 活跃任务覆盖判断。
- “检查已有文档/任务后创建”没有规范化指纹或 Redis 锁,重复请求会创建重复任务。
### DD-022 MongoDB 索引与设计不完全一致
- 缺少 Document `{state,updatedAt}`。
- Task 状态索引使用 `updatedAt`,设计为 `createdAt`。
- TaskDocument 唯一键不是设计中的 `{taskId,logicalPath}`。
- Artifact 没有唯一 CacheKey 索引。
- Package 缺少 `{status,createdAt}` 和 `expiresAt` 索引。
- 所有索引尚未设置固定名称,不利于显式迁移和审计。
### DD-023 自动化测试不足
至少需要增加:
- IANA 时区、固定半小时偏移、DST 跨越和 D+2 边界测试;
- Mutable 同轮合并、后续刷新、Frozen 复用、Dynamic 串行测试;
- 锁续租、租约丢失、旧 worker 晚提交和崩溃恢复测试;
- PostgreSQL 全量替换、Generation、空正文和回滚测试;
- RenderSlot 失效/发布竞态与 3 天过期测试;
- 取消、删除、人工重试、Package 7/30 天生命周期和权限测试;
- MongoDB/PostgreSQL/Redis/MinIO 集成测试。
## 6. P3:阻断全仓测试的既有源码问题
这些问题不是 DataDownload 引入,但当前会使 `go test ./...` 失败。
### REPO-001 PostgreSQL model 语法错误
- 文件:`service/models/model_pgsql/siteOriginalData.go:54`
- 问题:Go 语法错误,编译器报告 `unexpected keyword type`。
### REPO-002 FWH 代码生成命令引用失效配置
- 文件:`service/cmd/gen/fwh_pro.go`
- 问题:引用不存在的 `cfg.MySQLDSN`。
### REPO-003 PostgreSQL 代码生成命令引用失效配置
- 文件:`service/cmd/gen/pgsql/alarm_snapshot.go`
- 问题:引用不存在的 `cfg.PostgresDSN`。
### REPO-004 多个旧命令/测试调用旧 logger.Init 签名
- 范围:`service/cmd/test_plane_v2`、`service/repositories/mqtt/fwh_mqtt_test/test`、`service/test/shutdownStatistics`、`service/test/snapTest`、`service/test/getSnapTest`、`service/test/temp` 等。
- 问题:仍只传一个参数;当前签名要求环境和日志级别两个参数。
### REPO-005 getApowerSn 对指针切片直接 range
- 文件:`service/test/getApowerSn/getApowerSn.go`
- 问题:`res.Result` 为 `*[]domain.FwhSiteInfo`,需要先判空并解引用。
### REPO-006 getSnapTest 调用已删除函数
- 文件:`service/test/getSnapTest/getAlarmSnapshotTest.go`
- 问题:调用不存在的 `jobs.GetAlarmSnapshot`。
### REPO-007 temp 测试使用过期函数签名
- 文件:`service/test/temp/runtimeSort.go`
- 问题:`GetSiteAnalysisManageList` 的调用参数数量与当前签名不一致。
## 7. 建议处理批次
1. **并发正确性批次**:DD-001、DD-007、DD-008;DD-002、DD-003、DD-004、DD-005 已解决。
2. **存储模型批次**:DD-006、DD-009、DD-011、DD-016、DD-022。
3. **资源安全批次**:DD-010、DD-014、DD-015;DD-013 已解决。
4. **产品补全批次**:DD-017~DD-021。
5. **测试与仓库健康批次**:DD-023、REPO-001~REPO-007。