# 图片水印后缀异常兜底与补救方案 ## 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` 记录。 - 落地形式:优先做成后台管理可视化能力,支持异常筛选、预览、单条修复、批量修复、日志追踪。 该方案既能快速止血,也能在不扩散引用风险的前提下,稳妥完成历史数据修复。