Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions openspec/changes/update-signature-image-async-deletion/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 技术设计:签名图片异步删除(重命名标记 + 启动清理按文件名)

## 现状

- 图片写入:创建/更新/导入签名时,将图片保存到 `ConfigPath/signature/<sha1>.<ext>`,并把**完整文件路径**写入加密的签名数据字段 `CardImage`。
- 这里的 `<sha1>` 并非图片内容哈希:实际是对 `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___<original_filename>`(即在原文件名前增加固定前缀 `Delete___`)。
5. 重命名成功后,删除配置条目并返回成功。
6. 重命名失败:返回失败,配置不变。

> 说明:本变更不要求对“解密失败”的历史签名提供删除能力;删除操作在无法安全定位图片时失败是可接受的保守策略。

### 2) 启动清理:只清理 Delete___* 文件

- 扫描 `ConfigPath/signature` 目录,删除所有文件名以 `Delete___` 开头的文件。
- 不再:
- 遍历配置签名列表
- 解密签名数据
- 比对有效路径集合

## 兼容性

- 对现有图片文件命名无要求。
- 对配置结构无要求(不新增字段)。
- 仅改变“删除签名”和“启动清理”的行为语义。

## 失败语义(建议)

- 重命名失败:删除 API 返回失败并不删配置。
- 图片无法定位/不存在:返回失败并不删配置(避免“配置已删但图片未标记”的不一致)。

55 changes: 55 additions & 0 deletions openspec/changes/update-signature-image-async-deletion/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 提案:更新签名图片的异步删除机制(基于重命名标记)

## 背景与问题

当前签名名片图片的删除采用“启动后异步清理”的方式:启动时扫描 `ConfigPath/signature` 目录,将其与配置文件中签名列表解析出的图片路径集合进行对比,删除不在集合中的文件。

该机制存在关键缺陷:

- 清理逻辑依赖“可解密签名数据以得到图片路径”。当构建密钥变更导致历史签名数据无法解密时,有效路径集合可能为空或不完整,从而把大量仍应保留的图片误判为“孤立文件”并删除。
- 之前的“只要存在一个解析不通过就不删”的兜底会引入反向问题:只要配置中混入一个不同密钥的签名项,可能导致清理逻辑永远不删除任何文件。

## 目标

- 保留“异步删除”的总体设计(删除动作不在用户交互主流程做实际 `os.Remove`)。
- 删除签名时,确保其名片图片一定会在后续被清理,而不再依赖启动时解密配置来决定删除对象。
- 当重命名(标记删除)失败时,删除 API 直接返回失败,并且不删除配置中的签名条目,保证一致性。
- 启动后的清理逻辑改为“只按文件名决定删除”,不再依赖解密签名数据。

## 非目标

- 不引入同步删除(先删文件再删配置)的流程。
- 不做文件目录隔离或改变图片存储目录结构。
- 不尝试在启动清理阶段恢复或推断无法解密的签名图片归属。

## 方案概述

### 删除签名(仍为异步删除,但增加“重命名标记”)

- 当调用 `POST /signature/delete` 删除签名时:
- 后端从配置中读取该签名条目并解析其名片图片路径(若签名没有图片则跳过)。
- 后端定位图片文件时,优先使用签名数据中的路径;若路径为跨平台格式(如 Windows 反斜杠)或不可用,则回退按文件名(basename)在 `ConfigPath/signature` 下查找。
- 若图片文件存在,先将其重命名为 `Delete___<original_filename>`(即在原文件名前增加固定前缀 `Delete___`)。
- **只有当重命名成功(或无图片)时**,才删除配置中的签名条目并返回成功。
- 若重命名失败,则返回失败,并保留配置条目不变。

### 启动清理(按文件名前缀删除)

- 启动后清理任务仅扫描 `ConfigPath/signature` 目录:
- 删除所有文件名以 `Delete___` 开头的文件。
- 不再从配置读取签名列表、不再解密签名数据、不再比对“有效路径集合”。

## 风险与应对

- **重命名失败**:删除 API 会失败且不删除配置条目,用户可重试;不会出现“配置已删但图片未标记导致残留”的不一致。
- **旧版本遗留的孤立图片**:此方案不再清理由旧逻辑产生的“未引用但未标记”的文件;但在新行为下,正常删除签名将产生标记文件并被后续清理。
- **多签名共享同一图片文件**:经检查创建/更新/导入流程,图片文件名不是按“图片内容”去重,而是由 `id|name|originalImageName|timestamp`(导入为 `id|name|importFileName|timestamp`)参与生成,因此正常流程不会产生共享图片文件;若用户手工篡改配置导致共享引用,则删除其中一个签名的图片标记会影响其他引用者(异常数据,非本变更的兼容目标)。

## 验收标准

- 密钥变更后启动应用:不会因无法解密旧签名而误删 `ConfigPath/signature` 下的非 `Delete___*` 文件。
- 删除带图片的签名:
- 当图片重命名成功:签名配置条目被删除;启动后清理会删除该 `Delete___*` 文件。
- 当图片重命名失败:API 返回失败;签名配置条目不会被删除。
- 删除不带图片的签名:API 正常删除配置条目。

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 签名管理功能规格说明(变更:签名图片异步删除改为重命名标记 + 启动按文件名清理)

## MODIFIED Requirements

### Requirement: 删除签名

Normative: The system SHALL 在删除签名前尝试将该签名的名片图片重命名为 `Delete___<original_filename>` 作为删除标记(即在原文件名前增加固定前缀 `Delete___`);仅当重命名成功(或签名无图片)时才删除配置中的签名条目并返回成功;若重命名失败则 MUST 返回失败且 MUST NOT 删除配置条目。

#### Scenario: 删除带图片的签名(成功)

- **GIVEN** 签名条目存在且其 `CardImage` 指向 `ConfigPath/signature` 下的现有文件
- **WHEN** 前端调用 `POST /signature/delete`(负载 `{ id }`)
- **THEN** 后端将图片文件重命名为 `Delete___<original_filename>`
- **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** 普通图片文件不会因为“签名配置解密失败/缺失”而被删除

37 changes: 37 additions & 0 deletions openspec/changes/update-signature-image-async-deletion/tasks.md
Original file line number Diff line number Diff line change
@@ -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. 验证(按上述手工场景执行)

Loading
Loading