From c3ac3b2e60a19fece1dddc85fa3241ef4997beb2 Mon Sep 17 00:00:00 2001 From: LuSrackhall <3647637206@qq.com> Date: Thu, 8 Jan 2026 15:40:53 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=AE=9E=E7=8E=B0=E5=85=A8=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E7=AD=BE=E5=90=8D=E5=9B=BE=E7=89=87=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=9C=BA=E5=88=B6=EF=BC=8C=E9=80=9A=E8=BF=87?= =?UTF-8?q?=E9=87=8D=E5=91=BD=E5=90=8D=E6=A0=87=E8=AE=B0=E5=B9=B6=E6=8C=89?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=90=8D=E5=BC=82=E6=AD=A5=E6=B8=85=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E5=AF=86=E9=92=A5=E5=8F=98=E6=9B=B4?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E8=AF=AF=E5=88=A0=E4=BB=A5=E5=8F=8A=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E5=88=A0=E9=99=A4=E7=9A=84=E9=A3=8E=E9=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../design.md | 59 ++++++ .../proposal.md | 55 ++++++ .../specs/signature-management/spec.md | 52 ++++++ .../tasks.md | 37 ++++ sdk/server/signature_handlers.go | 171 +++++++++++++++++- sdk/signature/signature.go | 103 +++-------- 6 files changed, 396 insertions(+), 81 deletions(-) create mode 100644 openspec/changes/update-signature-image-async-deletion/design.md create mode 100644 openspec/changes/update-signature-image-async-deletion/proposal.md create mode 100644 openspec/changes/update-signature-image-async-deletion/specs/signature-management/spec.md create mode 100644 openspec/changes/update-signature-image-async-deletion/tasks.md diff --git a/openspec/changes/update-signature-image-async-deletion/design.md b/openspec/changes/update-signature-image-async-deletion/design.md new file mode 100644 index 00000000..1d353260 --- /dev/null +++ b/openspec/changes/update-signature-image-async-deletion/design.md @@ -0,0 +1,59 @@ +# 技术设计:签名图片异步删除(重命名标记 + 启动清理按文件名) + +## 现状 + +- 图片写入:创建/更新/导入签名时,将图片保存到 `ConfigPath/signature/.`,并把**完整文件路径**写入加密的签名数据字段 `CardImage`。 + - 这里的 `` 并非图片内容哈希:实际是对 `id|name|originalImageName|timestamp`(导入为 `id|name|importFileName|timestamp`)的字符串做 SHA-1。 + - 因此系统不做图片去重/共享:每次保存/导入都会生成新文件名,正常流程不会出现多个签名引用同一个图片文件。 +- 删除签名:当前删除接口只删除配置中的签名条目,不处理图片。 +- 启动清理:`CleanupOrphanCardImages` 会解密所有签名条目,收集 `CardImage` 路径作为“有效集合”,然后删除目录中不在集合内的文件。 + +## 问题根因 + +- 启动清理依赖解密成功。 +- 构建密钥变更导致历史签名解密失败时,“有效集合”不完整,清理会把仍应保留的图片误删。 + +## 设计原则 + +- **异步删除保持异步**:删除交互不做 `os.Remove`,只做“可回滚”的标记操作。 +- **删除对象由文件名决定**:启动清理不依赖配置、不依赖解密。 +- **一致性优先**:如果图片无法被标记(重命名失败),则拒绝删除配置条目。 + +## 新行为设计 + +### 1) 删除接口:先标记、后删配置 + +流程(针对有图片的签名): + +1. 读取配置中的签名条目。 +2. 解密签名数据,得到 `CardImage` 路径。 +3. 若 `CardImage` 为空:直接删除配置条目。 +4. 若 `CardImage` 非空: + - 校验路径必须位于 `ConfigPath/signature` 目录下(避免错误路径或路径注入)。 + - 若 `CardImage` 为跨平台格式(如 Windows `\\` 反斜杠路径)或路径不可用:回退按 basename 在 `ConfigPath/signature` 下定位。 + - 若最终仍无法定位图片文件:返回失败并拒绝删除配置条目(保持一致性)。 + - 重命名为 `Delete___`(即在原文件名前增加固定前缀 `Delete___`)。 +5. 重命名成功后,删除配置条目并返回成功。 +6. 重命名失败:返回失败,配置不变。 + +> 说明:本变更不要求对“解密失败”的历史签名提供删除能力;删除操作在无法安全定位图片时失败是可接受的保守策略。 + +### 2) 启动清理:只清理 Delete___* 文件 + +- 扫描 `ConfigPath/signature` 目录,删除所有文件名以 `Delete___` 开头的文件。 +- 不再: + - 遍历配置签名列表 + - 解密签名数据 + - 比对有效路径集合 + +## 兼容性 + +- 对现有图片文件命名无要求。 +- 对配置结构无要求(不新增字段)。 +- 仅改变“删除签名”和“启动清理”的行为语义。 + +## 失败语义(建议) + +- 重命名失败:删除 API 返回失败并不删配置。 +- 图片无法定位/不存在:返回失败并不删配置(避免“配置已删但图片未标记”的不一致)。 + diff --git a/openspec/changes/update-signature-image-async-deletion/proposal.md b/openspec/changes/update-signature-image-async-deletion/proposal.md new file mode 100644 index 00000000..4264cbd3 --- /dev/null +++ b/openspec/changes/update-signature-image-async-deletion/proposal.md @@ -0,0 +1,55 @@ +# 提案:更新签名图片的异步删除机制(基于重命名标记) + +## 背景与问题 + +当前签名名片图片的删除采用“启动后异步清理”的方式:启动时扫描 `ConfigPath/signature` 目录,将其与配置文件中签名列表解析出的图片路径集合进行对比,删除不在集合中的文件。 + +该机制存在关键缺陷: + +- 清理逻辑依赖“可解密签名数据以得到图片路径”。当构建密钥变更导致历史签名数据无法解密时,有效路径集合可能为空或不完整,从而把大量仍应保留的图片误判为“孤立文件”并删除。 +- 之前的“只要存在一个解析不通过就不删”的兜底会引入反向问题:只要配置中混入一个不同密钥的签名项,可能导致清理逻辑永远不删除任何文件。 + +## 目标 + +- 保留“异步删除”的总体设计(删除动作不在用户交互主流程做实际 `os.Remove`)。 +- 删除签名时,确保其名片图片一定会在后续被清理,而不再依赖启动时解密配置来决定删除对象。 +- 当重命名(标记删除)失败时,删除 API 直接返回失败,并且不删除配置中的签名条目,保证一致性。 +- 启动后的清理逻辑改为“只按文件名决定删除”,不再依赖解密签名数据。 + +## 非目标 + +- 不引入同步删除(先删文件再删配置)的流程。 +- 不做文件目录隔离或改变图片存储目录结构。 +- 不尝试在启动清理阶段恢复或推断无法解密的签名图片归属。 + +## 方案概述 + +### 删除签名(仍为异步删除,但增加“重命名标记”) + +- 当调用 `POST /signature/delete` 删除签名时: + - 后端从配置中读取该签名条目并解析其名片图片路径(若签名没有图片则跳过)。 + - 后端定位图片文件时,优先使用签名数据中的路径;若路径为跨平台格式(如 Windows 反斜杠)或不可用,则回退按文件名(basename)在 `ConfigPath/signature` 下查找。 + - 若图片文件存在,先将其重命名为 `Delete___`(即在原文件名前增加固定前缀 `Delete___`)。 + - **只有当重命名成功(或无图片)时**,才删除配置中的签名条目并返回成功。 + - 若重命名失败,则返回失败,并保留配置条目不变。 + +### 启动清理(按文件名前缀删除) + +- 启动后清理任务仅扫描 `ConfigPath/signature` 目录: + - 删除所有文件名以 `Delete___` 开头的文件。 + - 不再从配置读取签名列表、不再解密签名数据、不再比对“有效路径集合”。 + +## 风险与应对 + +- **重命名失败**:删除 API 会失败且不删除配置条目,用户可重试;不会出现“配置已删但图片未标记导致残留”的不一致。 +- **旧版本遗留的孤立图片**:此方案不再清理由旧逻辑产生的“未引用但未标记”的文件;但在新行为下,正常删除签名将产生标记文件并被后续清理。 +- **多签名共享同一图片文件**:经检查创建/更新/导入流程,图片文件名不是按“图片内容”去重,而是由 `id|name|originalImageName|timestamp`(导入为 `id|name|importFileName|timestamp`)参与生成,因此正常流程不会产生共享图片文件;若用户手工篡改配置导致共享引用,则删除其中一个签名的图片标记会影响其他引用者(异常数据,非本变更的兼容目标)。 + +## 验收标准 + +- 密钥变更后启动应用:不会因无法解密旧签名而误删 `ConfigPath/signature` 下的非 `Delete___*` 文件。 +- 删除带图片的签名: + - 当图片重命名成功:签名配置条目被删除;启动后清理会删除该 `Delete___*` 文件。 + - 当图片重命名失败:API 返回失败;签名配置条目不会被删除。 +- 删除不带图片的签名:API 正常删除配置条目。 + diff --git a/openspec/changes/update-signature-image-async-deletion/specs/signature-management/spec.md b/openspec/changes/update-signature-image-async-deletion/specs/signature-management/spec.md new file mode 100644 index 00000000..e504a39d --- /dev/null +++ b/openspec/changes/update-signature-image-async-deletion/specs/signature-management/spec.md @@ -0,0 +1,52 @@ +# 签名管理功能规格说明(变更:签名图片异步删除改为重命名标记 + 启动按文件名清理) + +## MODIFIED Requirements + +### Requirement: 删除签名 + +Normative: The system SHALL 在删除签名前尝试将该签名的名片图片重命名为 `Delete___` 作为删除标记(即在原文件名前增加固定前缀 `Delete___`);仅当重命名成功(或签名无图片)时才删除配置中的签名条目并返回成功;若重命名失败则 MUST 返回失败且 MUST NOT 删除配置条目。 + +#### Scenario: 删除带图片的签名(成功) + +- **GIVEN** 签名条目存在且其 `CardImage` 指向 `ConfigPath/signature` 下的现有文件 +- **WHEN** 前端调用 `POST /signature/delete`(负载 `{ id }`) +- **THEN** 后端将图片文件重命名为 `Delete___` +- **AND** 后端删除配置中的该签名条目并返回成功响应 +- **AND** 前端提示“删除成功”,签名在 SSE 推送后从列表移除 + +#### Scenario: 删除带图片的签名(重命名失败) + +- **GIVEN** 签名条目存在且其 `CardImage` 指向的文件无法被重命名或无法被定位(权限/占用/路径非法/跨平台路径不可解析等) +- **WHEN** 前端调用 `POST /signature/delete` +- **THEN** 后端返回失败响应 +- **AND** 后端 MUST NOT 删除配置中的该签名条目 +- **AND** 前端提示“删除失败”并保留原列表项 + +#### Scenario: 删除不带图片的签名 + +- **GIVEN** 签名条目存在且 `CardImage` 为空 +- **WHEN** 前端调用 `POST /signature/delete` +- **THEN** 后端删除配置中的该签名条目并返回成功响应 + +--- + +### Requirement: 签名图片路径初始化 + +Normative: The backend SHALL 在 `ConfigPath/signature` 下创建并使用签名图片目录;启动后的清理任务 MUST 删除所有文件名以 `Delete___` 开头的图片文件,且 MUST NOT 通过解密签名配置来推断需要删除的图片。 + +Non-normative note: +- 经检查后端实现,签名图片文件名不是基于图片内容去重,而是由 `id|name|originalImageName|timestamp`(导入为 `id|name|importFileName|timestamp`)参与生成,因此正常流程不会出现多个签名共享同一个图片文件。 + +#### Scenario: 初始化图片目录 + +- **GIVEN** 签名模块首次保存或更新图片 +- **WHEN** 后端写入文件 +- **THEN** 系统确保 `ConfigPath/signature` 目录存在 + +#### Scenario: 启动清理仅删除 Delete___* 文件 + +- **GIVEN** `ConfigPath/signature` 目录中同时存在 `Delete___*` 文件与普通图片文件 +- **WHEN** 启动后执行签名图片清理任务 +- **THEN** 所有 `Delete___*` 文件被删除 +- **AND** 普通图片文件不会因为“签名配置解密失败/缺失”而被删除 + diff --git a/openspec/changes/update-signature-image-async-deletion/tasks.md b/openspec/changes/update-signature-image-async-deletion/tasks.md new file mode 100644 index 00000000..c8bf08c2 --- /dev/null +++ b/openspec/changes/update-signature-image-async-deletion/tasks.md @@ -0,0 +1,37 @@ +# 任务清单 + +## 目标 + +实现“删除签名先重命名图片标记(`Delete___` 前缀),启动清理按文件名删除 Delete___*”的异步删除机制,避免密钥变更导致误删。 + +## 任务 + +1. 更新签名管理规格(spec delta) + - 修改“删除签名”与“清理孤立图片”两条 Requirement 的规范性描述与场景。 + +2. 后端:删除签名流程调整 + - 在 `POST /signature/delete` 中:删除配置前尝试对图片做 `os.Rename` 标记。 + - 重命名失败返回错误,且不删除配置条目。 + +3. 后端:启动清理逻辑调整 + - 将 `CleanupOrphanCardImages` 改为只删除 `ConfigPath/signature` 下文件名以 `Delete___` 开头的文件。 + +4. 日志与可观测性 + - 删除接口:记录重命名目标与失败原因。 + - 启动清理:记录删除数量。 + +5. 验证 + - 手工验证场景: + - 密钥变更后启动:非 Delete___ 文件不被删除。 + - 删除带图片签名:图片被重命名为 Delete___*;启动后被删除。 + - 模拟重命名失败:删除接口返回失败,配置未变。 + - 逻辑核对:创建/更新/导入流程不会生成“多签名共享同一图片文件”(文件名含 id 与时间戳,不做内容去重)。 + +## Checklist + +- [x] 1. 更新签名管理规格(spec delta) +- [x] 2. 后端:删除签名流程调整 +- [x] 3. 后端:启动清理逻辑调整 +- [x] 4. 日志与可观测性 +- [x] 5. 验证(按上述手工场景执行) + diff --git a/sdk/server/signature_handlers.go b/sdk/server/signature_handlers.go index 5efe4196..5601d3cd 100644 --- a/sdk/server/signature_handlers.go +++ b/sdk/server/signature_handlers.go @@ -30,6 +30,8 @@ import ( "io" "net/http" "os" + pathpkg "path" + "path/filepath" "strconv" "strings" @@ -325,7 +327,8 @@ func signatureRouters(r *gin.Engine) { // 类型转换并删除 if m, ok := signatureMap.(map[string]interface{}); ok { - if _, exists := m[req.ID]; !exists { + entryData, exists := m[req.ID] + if !exists { ctx.JSON(http.StatusNotFound, gin.H{ "success": false, "message": "签名不存在", @@ -333,6 +336,164 @@ func signatureRouters(r *gin.Engine) { return } + // 解析签名条目的加密 value(兼容新旧格式) + var encryptedValueStr string + if entry, ok := entryData.(map[string]interface{}); ok { + value, ok := entry["value"].(string) + if !ok || value == "" { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "签名数据格式错误", + }) + return + } + encryptedValueStr = value + } else if str, ok := entryData.(string); ok { + // 旧格式:直接是加密字符串 + encryptedValueStr = str + } else { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "签名数据格式错误", + }) + return + } + + // 解密签名数据以获取 CardImage 路径 + decryptedJSON, err := signature.DecryptValueWithDynamicKey(encryptedValueStr, req.ID) + if err != nil { + logger.Debug("动态密钥解密失败,尝试旧方式", "error", err.Error()) + decryptedJSON, err = signature.DecryptData(encryptedValueStr, signature.GetKeyA()) + if err != nil { + logger.Error("签名数据解密失败,无法标记删除图片", "error", err.Error()) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "签名数据解密失败,删除失败", + }) + return + } + } + + var sigData signature.SignatureData + if err := json.Unmarshal([]byte(decryptedJSON), &sigData); err != nil { + logger.Error("签名数据解析失败,无法标记删除图片", "error", err.Error()) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "签名数据解析失败,删除失败", + }) + return + } + + // 若存在图片:先重命名为 Delete___ 前缀标记;标记失败则不删除配置 + const deletePrefix = "Delete___" + signatureDir := filepath.Join(config.ConfigPath, "signature") + if strings.TrimSpace(sigData.CardImage) != "" { + // 这里是本变更的核心: + // - “签名删除”仍是异步删除:我们不直接 os.Remove 图片,只做“重命名标记” + // - 只有当标记成功(或图片已被标记)时,才允许删除配置条目 + // + // 关于“是否存在多个签名共享同一图片文件”的结论(来自创建/导入流程的实际实现): + // - CreateSignature / UpdateSignature / ImportSignature 在保存图片时,文件名是对 + // `id|name|originalImageName|timestamp`(导入为 `id|name|importFileName|timestamp`)做 SHA-1。 + // - 这意味着图片文件名并非基于图片内容哈希;每次保存/导入都会生成新文件名,正常流程下 + // 不会出现“多个签名引用同一个图片文件”的共享/去重。 + // - 若用户/外部程序手工篡改配置导致多个签名 CardImage 指向同一路径,则本接口标记删除 + // 会影响其他仍引用该文件的签名(这属于异常数据,不在本需求的兼容范围内)。 + // + // 为什么需要“basename 回退定位”? + // - 历史签名可能在不同平台生成/迁移(例如 Windows 路径包含 `\\` 或盘符) + // - 在 macOS/Linux 上用 os.Stat 直接检查这样的路径会失败,导致之前实现“跳过标记但仍删除配置” + // 从而出现:签名条目删了,但图片未被重命名标记(你手测遇到的问题) + // - 解决方式:优先使用可用的绝对路径;若不可用,则按图片文件名在 signatureDir 下回退查找。 + cardImageRaw := strings.TrimSpace(sigData.CardImage) + + // 1) 解析文件名(兼容 Windows `\` 与 POSIX `/` 两种分隔符) + // - filepath.Base 在 Unix 下不会识别 `\` 为分隔符,所以先把 `\` 归一化为 `/`,再用 path.Base。 + normalized := strings.ReplaceAll(cardImageRaw, "\\", "/") + baseName := pathpkg.Base(normalized) + if baseName == "." || baseName == "/" || baseName == "" { + logger.Error("无法解析签名图片文件名,拒绝删除", "cardImage", cardImageRaw) + ctx.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "签名图片路径非法,删除失败", + }) + return + } + + // 2) 若图片本身已被标记(Delete___ 前缀),则无需重复标记,可继续删除配置条目 + if !strings.HasPrefix(baseName, deletePrefix) { + // 3) 解析并定位“当前图片文件的真实路径” + // candidates 的顺序代表“优先级”。 + var candidates []string + + // 3.1) 若 CardImage 看起来像当前平台的绝对路径,则优先尝试它 + // 但要注意:Windows 盘符路径(如 C:\\...)在 Unix 上不是绝对路径,不能当成 filepath.IsAbs。 + looksLikeWindowsAbs := len(cardImageRaw) >= 3 && cardImageRaw[1] == ':' && (cardImageRaw[2] == '\\' || cardImageRaw[2] == '/') + if filepath.IsAbs(cardImageRaw) && !looksLikeWindowsAbs { + candidates = append(candidates, filepath.Clean(cardImageRaw)) + } else { + // 3.2) 否则按“相对路径”拼到 signatureDir 下(如果 cardImageRaw 本来就是相对路径,这条能命中) + // 注意:这里并不信任 raw,后面会用 Rel 再做一次“必须在 signatureDir 下”的校验。 + candidates = append(candidates, filepath.Join(signatureDir, filepath.Clean(cardImageRaw))) + } + + // 3.3) 最后按 basename 回退:signatureDir/ + // 这是跨平台迁移时最可靠的命中方式(图片文件实际就存放在 signatureDir 下)。 + candidates = append(candidates, filepath.Join(signatureDir, baseName)) + + var foundPath string + for _, c := range candidates { + cleanPath := filepath.Clean(c) + rel, err := filepath.Rel(signatureDir, cleanPath) + if err != nil || strings.HasPrefix(rel, "..") { + // 不允许标记 signatureDir 以外的任何路径,避免误操作/路径注入 + continue + } + st, err := os.Stat(cleanPath) + if err != nil { + continue + } + if st.IsDir() { + continue + } + foundPath = cleanPath + break + } + + // 4) 只要签名数据声明“有图片”,但我们无法定位到真实文件,就必须失败并拒绝删除配置 + // 这是为了满足规范:只有标记成功(或无图片)才能删除签名条目。 + if foundPath == "" { + logger.Error("无法定位签名图片文件,拒绝删除配置", "cardImage", cardImageRaw, "signatureDir", signatureDir) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "无法定位签名图片文件,删除失败", + }) + return + } + + // 5) 执行重命名标记(Delete___ 前缀) + newPath := filepath.Join(signatureDir, deletePrefix+baseName) + if _, err := os.Stat(newPath); err == nil { + // 避免覆盖既有文件:若目标已存在,直接失败(让用户/程序显式处理) + logger.Error("删除标记目标文件已存在,拒绝覆盖", "to", newPath) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "删除标记冲突,删除失败", + }) + return + } + if err := os.Rename(foundPath, newPath); err != nil { + logger.Error("标记删除签名图片失败", "from", foundPath, "to", newPath, "error", err.Error()) + ctx.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "标记删除签名图片失败,删除失败", + }) + return + } + logger.Info("已标记删除签名图片", "from", foundPath, "to", newPath) + } + } + // 删除签名 delete(m, req.ID) @@ -956,10 +1117,10 @@ func signatureRouters(r *gin.Engine) { } matchedSignatures = append(matchedSignatures, map[string]string{ - "encryptedId": encryptedID, - "qualificationCode": qualCode, - "qualificationFingerprint": signature.GenerateQualificationFingerprint(qualCode), - "name": sigName, + "encryptedId": encryptedID, + "qualificationCode": qualCode, + "qualificationFingerprint": signature.GenerateQualificationFingerprint(qualCode), + "name": sigName, }) } } diff --git a/sdk/signature/signature.go b/sdk/signature/signature.go index 169a2fc6..5bd6616e 100644 --- a/sdk/signature/signature.go +++ b/sdk/signature/signature.go @@ -537,13 +537,25 @@ func decryptData(encryptedData string, key []byte) (string, error) { return string(plaintext), nil } -// CleanupOrphanCardImages 清理不在配置中的孤立签名图片 -// 该函数会扫描 signature 目录下的所有文件, -// 与配置中的签名数据进行比对,删除不在配置中的图片文件 -// encryptionKey: 保留用于兼容旧调用,实际使用KeyA获取动态密钥 +// CleanupOrphanCardImages 清理签名图片的“删除标记”文件 +// +// 重要语义(与历史实现不同): +// - 本函数不再解密/遍历配置中的签名列表,也不再尝试推断“孤立图片”。 +// - 它只扫描 `ConfigPath/signature` 目录并删除文件名以 `Delete___` 开头的文件。 +// +// 关于“图片共享/去重”的实现现状(用于避免 review 误解): +// - 创建/更新/导入签名保存图片时,文件名由 `id|name|originalImageName|timestamp` 参与计算, +// 并非按图片内容做去重;因此正常流程下不会出现多个签名共享同一图片文件。 +// +// 设计原因:当构建密钥变更导致历史签名解密失败时,旧的“解密配置 -> 收集有效路径 -> 删除不在集合中的文件” +// 会误删仍应保留的图片;改为按文件名前缀删除可从根上规避该风险。 +// +// encryptionKey: 历史兼容保留;当前实现不再使用该参数。 func CleanupOrphanCardImages(encryptionKey []byte) error { logger.Info("开始执行签名名片图片清理操作...") + const deletePrefix = "Delete___" + // 1. 获取 signature 目录路径 signatureDir := filepath.Join(config.ConfigPath, "signature") @@ -553,75 +565,14 @@ func CleanupOrphanCardImages(encryptionKey []byte) error { return nil } - // 2. 从配置中获取所有签名数据,解析出有效的图片路径集合 - validImagePaths := make(map[string]bool) - signatureMapValue := config.GetValue("signature") - - if signatureMapValue != nil { - if signatureMap, ok := signatureMapValue.(map[string]interface{}); ok { - // 遍历所有的签名配置 - for encryptedID, v := range signatureMap { - var encryptedValueStr string - - // 兼容新格式 SignatureStorageEntry - if entry, ok := v.(map[string]interface{}); ok { - if value, ok := entry["value"].(string); ok { - encryptedValueStr = value - } else { - logger.Warn("无法从 SignatureStorageEntry 中提取 value 字段") - continue - } - } else if str, ok := v.(string); ok { - // 兼容旧格式:直接是加密字符串 - encryptedValueStr = str - } else { - logger.Warn("无法识别签名数据格式") - continue - } - - // 解密签名数据(使用动态密钥) - var decryptedData string - var err error - - // 尝试使用新的动态密钥解密 - decryptedData, err = DecryptValueWithDynamicKey(encryptedValueStr, encryptedID) - if err != nil { - // 如果失败,尝试使用旧的方式(KeyA) - logger.Debug("动态密钥解密失败,尝试旧方式", "error", err.Error()) - keyA := GetKeyA() - decryptedData, err = decryptData(encryptedValueStr, keyA) - if err != nil { - logger.Warn("签名数据解密失败,跳过此签名", "error", err.Error()) - continue - } - } - - // 解析 JSON 数据 - var sigData SignatureData - if err := json.Unmarshal([]byte(decryptedData), &sigData); err != nil { - logger.Warn("签名数据 JSON 解析失败,跳过此签名", "error", err.Error()) - continue - } - - // 如果有图片路径,添加到有效路径集合 - if sigData.CardImage != "" { - validImagePaths[sigData.CardImage] = true - logger.Debug("记录有效的签名图片路径", "path", sigData.CardImage) - } - } - } - } - - logger.Info("配置中的有效签名图片数量", "count", len(validImagePaths)) - - // 3. 获取 signature 目录下的所有文件 + // 2. 获取 signature 目录下的所有文件 files, err := os.ReadDir(signatureDir) if err != nil { logger.Error("读取签名目录失败", "error", err.Error()) return err } - // 4. 遍历目录中的所有文件,删除不在有效路径中的文件 + // 3. 遍历目录中的所有文件,删除以 Delete___ 开头的标记文件 deletedCount := 0 for _, file := range files { // 只处理文件,跳过目录 @@ -629,17 +580,17 @@ func CleanupOrphanCardImages(encryptionKey []byte) error { continue } + if !strings.HasPrefix(file.Name(), deletePrefix) { + continue + } + filePath := filepath.Join(signatureDir, file.Name()) - // 检查文件路径是否在有效路径集合中 - if _, exists := validImagePaths[filePath]; !exists { - // 文件不在有效路径中,删除它 - if err := os.Remove(filePath); err != nil { - logger.Warn("删除孤立图片文件失败", "path", filePath, "error", err.Error()) - } else { - logger.Debug("成功删除孤立图片文件", "path", filePath) - deletedCount++ - } + if err := os.Remove(filePath); err != nil { + logger.Warn("删除标记图片文件失败", "path", filePath, "error", err.Error()) + } else { + logger.Debug("成功删除标记图片文件", "path", filePath) + deletedCount++ } }