Files
wucaixing-backend/docs/图片水印后缀异常兜底与补救方案.md
shihongwei 0adba6f5cb feat(photo-evidence): 实现图片水印异常修复全流程功能
抽离并重构原有图片水印生成逻辑,统一输出jpeg格式,避免因异常文件后缀产生坏数据
新增异常水印图片的分页查询、预览确认、单条修复、批量异步修复及任务状态查询接口
新增对应的控制器、业务服务类与数据传输对象
补充图片水印异常修复的完整方案文档
2026-07-01 23:02:00 +08:00

422 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 图片水印后缀异常兜底与补救方案
## 1. 背景
现有图片取证水印流程位于 [PhotoEvidenceServiceImpl.java:L51-L149](file:///g:/code/shw/hot/wucaixing-full/wucaixing-backend/src/main/java/com/hotwj/platform/common/photoEvidence/service/impl/PhotoEvidenceServiceImpl.java#L51-L149)。
当前已确认的问题链路如下:
- 前端上传 OSS 时,部分原始图片没有正确的文件后缀名。
- 原始 `sys_oss.file_suffix` 被写成了 `2``3``.2``.3` 这一类异常值。
- 后端在生成水印图时,直接使用原始 `file_suffix` 作为 `ImageIO.write` 的输出格式。
- 生成的临时文件可能为空文件,最终上传到 OSS 后变成 `0B`
- 新生成的 `sys_oss` 记录中,`ext1.fileSize=0``contentType=null`,页面预览和下载均异常。
示例数据可见 [data.json:L1-L35](file:///g:/code/shw/hot/wucaixing-full/data.json#L1-L35)
- 原图记录 `file_suffix=3`,但 `ext1.contentType=image/jpeg`
- 水印图记录 `file_suffix=.3``ext1.fileSize=0``contentType=null`
当前风险点位于 [PhotoEvidenceServiceImpl.java:L137-L149](file:///g:/code/shw/hot/wucaixing-full/wucaixing-backend/src/main/java/com/hotwj/platform/common/photoEvidence/service/impl/PhotoEvidenceServiceImpl.java#L137-L149)
,源码中也已经有 `TODO` 提示需要判断 `mime`
## 2. 根因分析
本问题的根因不是“没有后缀名”本身,而是“后端把不可信的后缀名继续当成图片编码格式使用”。
具体来说:
- 原始图片是否可读,真正应该依据的是二进制内容和 MIME不应该只依赖文件名后缀。
- 水印图不是前端上传文件,而是后端重新生成的系统产物,其输出格式应由后端统一决定。
- 只要继续信任 `sys_oss.file_suffix`,就会持续受到历史脏数据和前端上传异常的影响。
因此,兜底思路必须从“猜正确后缀”改成“后端固定输出规范”。
## 3. 线上兜底方案
### 3.1 目标
阻断新增坏数据,确保后续所有水印图都能稳定生成、上传、展示和下载。
### 3.2 方案原则
- 原图校验看内容,不看后缀。
- 水印图输出格式由后端统一指定,不继承原图后缀。
- 上传时必须带明确的 `contentType`
- 入库前必须校验结果文件非空。
### 3.3 推荐方案
建议将“图片取证水印图”统一输出为 `jpg/jpeg`
原因:
-
当前水印结果图在 [PhotoEvidenceServiceImpl.java:L261-L283](file:///g:/code/shw/hot/wucaixing-full/wucaixing-backend/src/main/java/com/hotwj/platform/common/photoEvidence/service/impl/PhotoEvidenceServiceImpl.java#L261-L283)
中已经固定为 `BufferedImage.TYPE_INT_RGB`,天然适合输出为 `JPEG`
- 业务场景是现场拍照取证,统一输出 `jpg` 与实际使用习惯一致。
- 可以彻底摆脱 `2``3``.2``.3` 这一类脏后缀的影响。
- 后续历史修复和排障标准会非常统一。
### 3.4 兜底规则
建议按以下规则处理:
1. 下载原始 OSS 文件后,先尝试按图片内容解析。
2. 只要原图字节能正常解析成图片,就允许继续加水印。
3. 水印结果图统一编码为 `jpeg`
4. 临时文件统一使用 `.jpg` 后缀。
5. 上传 OSS 时统一使用 `contentType=image/jpeg`
6. 上传前必须校验:
- `ImageIO.write` 返回成功
- 临时文件存在
- 临时文件大小大于 `0`
7. 任一校验失败,直接报错,不创建 `HotPhotoEvidence` 记录,不创建新的水印 `sys_oss` 记录。
### 3.5 兜底后的收益
- 前端即使继续上传无后缀文件,后端仍可稳定产出标准水印图。
- 生成的水印图始终具备标准后缀和标准 `contentType`
- 不再出现 `.2``.3` 这类系统生成的异常文件。
- 新增数据和历史补救数据将收敛到同一规范。
## 4. 历史补救总原则
### 4.1 必须满足的约束
本次历史补救必须满足以下硬约束:
- 必须替换原有坏掉的 `sys_oss` 记录内容。
- 必须保持原 `oss_id` 不变。
- 不能只改 `HotPhotoEvidence` 表中的 `watermarked_oss_id` 指向。
- 不能新增一条新的 `sys_oss` 记录再让业务表改引用。
原因:
- 这个 `oss_id` 不仅存在于 `HotPhotoEvidence`,还可能被其他业务表、日志记录、导出记录、接口缓存、前端页面配置等位置复用。
- 如果改成新 `oss_id`,补救范围会迅速扩散到未知引用方,风险不可控。
- 保持原 `oss_id` 不变,才能把历史补救范围收敛在“文件对象替换 + 原记录修补”这一层。
### 4.2 正确的补救思路
正确思路不是“替换原始图的 `sys_oss` 记录”,而是:
-`HotPhotoEvidence` 为主表定位坏掉的水印记录。
- 通过 `original_oss_id` 找回原始图片。
- 重新生成一张标准的水印图。
- 上传到 OSS 获得新的对象文件。
- 然后覆盖原 `watermarked_oss_id` 对应的 `sys_oss` 记录内容。
- `oss_id` 保持不变,只更新该记录指向的对象信息和元数据。
换句话说:
- 替换的是“坏水印图对应的 `sys_oss` 内容”
- 保留的是“该 `sys_oss` 的主键 `oss_id`
## 5. 历史补救识别范围
建议先做数据筛查,再执行修补。
### 5.1 强特征
以下记录可直接视为高风险坏数据:
- `sys_oss.ext1.fileSize = 0`
- `sys_oss.file_suffix` 为纯数字,如 `2``3`
- `sys_oss.file_suffix``.数字`,如 `.2``.3`
- `sys_oss.original_name``photo_evidence_xxx.2``photo_evidence_xxx.3`
- `sys_oss.ext1.contentType is null`
### 5.2 建议以 `HotPhotoEvidence` 为主表驱动
原因:
- 该表已经保留了 `originalOssId``watermarkedOssId`
- 已经保留了 `captureTimeClient``captureTimeServer`
- 已经保留了 `latitude``longitude`
- 已经保留了 `addressTextServer``addressTextClient``addressSource`
这些字段足够支撑“按历史状态重建水印图”,不需要重新依赖前端数据。
## 6. 历史补救执行方案
### 6.1 核心目标
在不改变 `watermarked_oss_id``oss_id` 的前提下,修复坏掉的水印图对象和其 `sys_oss` 元数据。
### 6.2 推荐执行步骤
1. 根据 `HotPhotoEvidence.watermarked_oss_id` 关联 `sys_oss`,筛出疑似坏水印记录。
2. 按记录读取 `original_oss_id` 对应的原始图。
3. 使用历史记录中的 `captureTimeClient` 作为水印时间。
4. 水印地址按以下优先级确定:
-`addressSource=SERVER`,取 `addressTextServer`
- 否则取 `addressTextClient`
5. 不重新调用逆地理接口,不重算地址。
6. 按统一规范重新生成 `jpg` 水印图。
7. 先上传到 OSS 新对象路径,确认对象可访问且大小正常。
8. 用新对象信息覆盖旧的 `sys_oss` 记录:
- `file_name`
- `url`
- `file_suffix`
- `original_name`
- `ext1.fileSize`
- `ext1.contentType`
- 如有必要同步更新时间和更新人
9. `oss_id` 保持不变。
10. 数据更新成功后,再删除旧的坏对象文件,或先保留待统一清理。
### 6.3 必须覆盖的字段
覆盖原 `sys_oss` 记录时,建议至少同步修正以下内容:
- `file_name`:替换为新上传对象键
- `url`:替换为新对象地址
- `file_suffix`:统一改为 `.jpg`
- `original_name`:统一规范化,例如 `photo_evidence_{ossId}.jpg`
- `ext1.fileSize`:更新为真实大小
- `ext1.contentType`:更新为 `image/jpeg`
如有以下字段被业务使用,也建议同步补齐:
- `ext1.source`
- `ext1.remark`
- `update_time`
- `update_by`
### 6.4 为什么不要重新生成新的 `sys_oss` 记录
- 会引入新的 `oss_id`
- 会导致其他业务表引用失效
- 会让修复脚本必须跨全库改引用
- 很难保证没有漏网表
- 回滚和审计也会变复杂
因此,历史补救必须采用“新对象文件 + 原记录覆盖”的模式。
## 7. 前端可视化补救方案
为了让补救方案易于操作,建议将历史补救放到后台管理界面,而不是只做一次性脚本。
### 7.1 推荐入口
建议新增后台管理页面,例如:
- 菜单名称:`水印图片修复`
- 所属模块:`系统管理``附件管理`
- 页面定位:面向运维、实施、研发,不对普通业务用户开放
前端落点建议明确放到维护模块中,参考现有维护页 [index.vue](file:///g:/code/shw/hot/wucaixing-full/wucaixing-frontend/src/views/system/maintenance/index.vue)
的维护项列表能力。
建议方案:
- 在前端维护模块 `wucaixing-frontend/src/views/system/maintenance/index.vue` 中新增一个维护项,例如 `图片水印修复`
- 点击维护项后进入专门的修复弹窗或修复页面
- 该页面承载异常筛选、预览比对、勾选记录、单条修复、批量修复、任务进度和结果查看
这样可以与现有 `媒体资源元数据修复` 的运维入口保持一致,便于统一权限、统一操作习惯、统一审计。
### 7.2 列表页建议字段
列表页建议展示以下信息,方便快速识别和人工核验:
- `evidenceId`
- `watermarkedOssId`
- `originalOssId`
- 原始图片缩略图
- 当前水印图缩略图
- `sys_oss.file_suffix`
- `ext1.fileSize`
- `contentType`
- `captureTimeClient`
- `addressSource`
- 地址文本
- 公司 / 业务类型 / 业务 ID
- 创建时间
- 修复状态
- 修复结果说明
### 7.3 列表页筛选条件
建议支持以下筛选项:
- 仅看异常记录
-`watermarkedOssId`
-`originalOssId`
- 按公司
- 按业务类型
- 按时间范围
-`fileSize=0`
-`contentType为空`
-`file_suffix` 异常
- 按修复状态
### 7.4 页面操作按钮
建议至少提供以下操作:
- `预览原图`
- `预览当前水印图`
- `生成修复预览`
- `勾选当前记录`
- `批量勾选筛选结果`
- `清空勾选`
- `执行单条修复`
- `批量修复已勾选记录`
- `导出异常清单`
- `查看修复日志`
补充要求:
- 不允许用户在未查看修复前后效果的情况下直接发起批量修复
- 批量修复应以“用户已勾选且已完成预览确认的记录”为执行范围,而不是直接对整个筛选结果无差别执行
### 7.5 单条修复建议流程
单条修复推荐流程如下:
1. 用户点开一条异常记录。
2. 页面展示:
- 原图预览
- 当前坏水印图预览
- 拟修复后的新水印预览
- 修复前后元数据对比
3. 用户点击 `确认修复`
4. 后端执行:
- 读取原图
- 按历史时间与地址重新加水印
- 上传新 OSS 对象
- 覆盖原 `sys_oss` 记录
- 记录操作日志
5. 页面回显修复结果。
这个流程适合先做试点修复和人工验证。
### 7.6 批量修复建议流程
批量修复推荐流程如下:
1. 用户先通过筛选条件锁定异常数据范围。
2. 页面展示总数、可修复数、疑似失败风险数,并支持逐条勾选待修复记录。
3. 用户先对准备修复的记录执行预览,页面必须展示:
- 原图
- 当前坏水印图
- 拟修复后的新水印图
- 修复前后元数据差异
4. 用户确认预览效果无误后,勾选本次真正要修复的记录。
5. 用户点击 `批量修复已勾选记录` 前,系统弹出二次确认,并明确显示本次修复条数。
6. 后端采用任务方式异步执行,页面可轮询查看进度。
7. 每条记录输出修复结果:
- 成功
- 原图不存在
- 原图无法解析
- 新水印生成失败
- OSS 上传失败
-`sys_oss` 覆盖失败
8. 任务完成后支持导出结果明细。
这里的关键要求是:
- 批量修复之前必须先有“预览确认”步骤
- 批量修复的对象必须是“用户主动勾选的记录”
- 不能仅凭筛选条件直接全量执行修复任务
### 7.7 为什么前端界面比一次性脚本更适合
- 更适合先小批量验证,再逐步扩大范围
- 操作人员无需直接跑脚本和改 SQL
- 每条记录可预览、可追踪、可重试
- 更方便做权限控制和操作审计
- 后续若再出现新脏数据,也可以复用同一页面处理
## 8. 后端接口建议
为支撑前端可视化操作,建议至少提供以下几类接口:
### 8.1 异常列表接口
用途:
- 按筛选条件查询疑似异常水印记录
- 返回分页列表和异常原因标签
### 8.2 预览接口
用途:
- 根据 `evidenceId``watermarkedOssId` 生成“拟修复水印图”的临时预览
- 不落库,不覆盖 `sys_oss`
### 8.3 单条修复接口
用途:
- 对单条异常记录执行正式修复
- 修复成功后覆盖原 `sys_oss`
### 8.4 批量修复任务接口
用途:
- 创建批量修复任务
- 入参必须是前端勾选确认后的记录 ID 列表,而不是只有筛选条件
- 后台异步执行
- 支持查询进度、成功数、失败数、失败原因
### 8.5 操作日志接口
用途:
- 记录谁在什么时间修复了哪条记录
- 保留修复前后对象信息,便于追溯
## 9. 操作审计与回滚建议
### 9.1 建议记录的审计信息
每次修复建议记录:
- 操作人
- 操作时间
- `evidenceId`
- `watermarkedOssId`
-`file_name`
-`file_name`
-`url`
-`url`
-`ext1`
-`ext1`
- 修复结果
- 错误信息
### 9.2 回滚方式
由于要求保持原 `oss_id` 不变,回滚方案建议也按“对象替换”处理:
1. 修复前先记录旧对象信息。
2. 如修复后发现异常,可重新把旧对象重新上传或恢复。
3. 再次覆盖同一个 `sys_oss` 记录,使其回到修复前状态。
也就是说,回滚不是改回旧 `oss_id`,而是把同一个 `oss_id` 再次指回旧对象。
## 10. 推荐实施顺序
建议按以下顺序落地:
1. 先完成线上兜底,阻断新增坏数据。
2. 再开发后台可视化补救页面和补救接口。
3. 先选少量样本做单条修复验证。
4. 确认预览、下载、页面展示全部正常后,再进行批量修复。
5. 批量修复完成后,统一清理被替换掉的坏 OSS 对象。
## 11. 最终结论
本问题的正确处理方式是:
- 线上新增场景:后端不再信任原始上传后缀,水印图统一输出为 `jpg/jpeg`
- 历史补救场景:以 `HotPhotoEvidence` 为主表重建水印图,但不能改 `oss_id`,必须覆盖原坏水印图对应的 `sys_oss` 记录。
- 落地形式:优先做成后台管理可视化能力,支持异常筛选、预览、单条修复、批量修复、日志追踪。
该方案既能快速止血,也能在不扩散引用风险的前提下,稳妥完成历史数据修复。