lovife
|
12067b8ae4
|
fix(file): 修掉 EXIF 朝向 bug(竖拍照片会躺倒)+ 上传改一步 + 默认不留原始件
一、客户决定:不用保持原件,只要压缩满足效果
fill.storage.keep-raw 开关,默认 false。交付物就是压缩后的规范件。
顺带把上传从两步变一步:
原来 upload(存原图)→ process(读原图→压缩→存规范件)
现在 upload(收上来→立刻压缩→只存规范件)
留原始件是两步存在的**唯一理由**;不要它之后这步只剩代价
(多一个"传了但没处理"的中间态、多一次往返、多一份存储)。
process 保留为"重新规范化",但没有原始件时**直接返回现状而不报错**。
二、🐛 抓到一个会命中绝大多数真实照片的 bug:EXIF 朝向
手机竖拍时**像素其实是横的** + 一个 Orientation=6(显示时转 90°)。
浏览器认这个标记所以看着是正的;而 ImageIO **不读 EXIF**、重编码又丢掉它 →
输出成了"像素横着、也没有标记"的图 → **机构看到的是一张转了 90° 的身份证**。
实测(修前):输入 3200×2400(横)+ O=6 → 输出 1600×1200(仍横)、EXIF=None ❌
修后:1200×1600(竖)✓ 且输出不再带 EXIF(否则会被转两次)
**为什么 E2E 一直没抓到**:测试图都是程序生成的、不带 EXIF;
而真实学生手机拍的竖拍照片几乎全带 —— 这个 bug 会命中绝大多数真实材料,
是靠"想到这一层"发现的,不是靠测试。
实现:module/file/service/ExifOrientation.java
· 自己解析 JPEG 的 APP1/EXIF(只用一个 tag,三十行,不引新依赖)
· **读不出来时一律"不转"** —— 宁可保持原样,也不凭猜测把图转错
· 八种朝向用**逐像素索引映射**,不用 AffineTransform:
索引一看就懂、容易对着规范核;AffineTransform 写错了很难发现(图还是能出,只是方向不对)
· **转正必须在任何缩放之前** —— 转置会互换宽高,尺寸计算得基于转正后的
三、为"前端压缩"铺路的关键改动
compress() 现在会判断"是不是已经达标了":
没触发缩放 && 没有 EXIF 需转正 && 体积本来就在上限内 → **原样返回,不重编码**
因为前端马上会按材料项目标先压一遍(会转正并去掉 EXIF),服务端再压就是
**JPEG 二次编码** —— 白掉一次画质。quality 用 NOT_REENCODED=0 标记"没重编码过",
排查"这张图为什么有点糊"时能一眼看出被压过几次。
这条对 P4 截图尤其合适:原样保持 PNG,表格线一点不糊。
四、顺手修的
· url/content 的 original=true:没有原始件时**优雅退回到交付件**并如实标注
(FileUrl 加 original 字段)—— 学生点"看原图"只是想看自己传的那份,
报"文件不存在"是纯粹的困惑
· sniffExt():按**字节幻数**判类型,不信文件名后缀(学生传的 .jpg 里装 PNG 是常事,
而这个结论会写进数据库、决定导出包的扩展名)
五、E2E
e2e-m3-file 37/37,新增第 7 节专守 EXIF:
输入 3200×2400(横)+ O=6 → 输出 1200x1600
✓ 输出是竖的 ✓ 长边仍受 P2 约束 ✓ 不再带 EXIF
原始件相关断言改成**按策略断言**(开/关两种都对),而不是写死"必须保留"——
那会让"改配置"看起来像"代码坏了"。
全量 467 条断言、0 失败。
⏳ 还没做:**前端压缩**("传 800KB 而不是 3MB")。本轮做的是为它铺路:
服务端不再二次编码 + EXIF 转正机制已就位(前端要用 JS 做同样的事)。
前端要做:照片类读 EXIF→canvas 旋正→长边 1600→q85;P1/P4 不压直传;
压完仍超就回退原图让服务端兜底。
⚠️ 流程坑(本轮白花三次编译):**不要把 Java 字符串字面量放进 PowerShell here-string
传给 Python** —— @"..."@ 会把 " 吃掉(return "bin"; 变成 return bin;)。
带引号的代码一律用 edit 工具改,不要过转义层。
|
2026-09-29 15:58:08 +08:00 |
|
lovife
|
d8463a051f
|
verify(storage): 七牛云接通并验证通过 —— 对象存储这块完成
根因就是那个"多出来的一个字符":secret-key 原来是 **41** 字符,
而七牛的 AK/SK 都是 40 字符的 URL 安全 base64。用户重新复制后(SK 40 字符)一切正常。
排查方式值得记:没有凭一次失败下结论,而是做了三次互相独立的验证 ——
① 我手写的管理 API 签名(可能是我的 bug,不能作数)
② 应用启动自检里的**七牛官方 SDK** 的 uploadToken
③ 一个**能把原因分开**的调用:BucketManager.stat(bucket, 一个一定不存在的键)
612 = 鉴权通过(只是文件不存在)/ 401 = 密钥无效 / 631 = bucket 名不对
三次一致指向"密钥无效",所以给用户的结论是确定的,而不是"可能是配置问题"。
验证证据:
启动自检通过:写入 → 存在 → 读回 → 删除 全部正常(provider=qiniu)
材料存储:七牛云(bucket=tools-matrix domain=https://file.tools.coding.design)
e2e-m1-storage.py 16/16:
· 签名链接形如 .../xiaotutongxue/std/1/17/%E5%BC%A0%E5%90%8C%E5%AD%A6-…?e=…&token=…
· **中文对象键逐段编码正确**(就是设计时专门防的那个坑)
· 签名链接下载回来是真 JPEG、非空
· **不带签名的裸地址被拒(401/403)→ 空间确实私有,签名在起作用**
· 对象键带 xiaotutongxue/ 前缀
· **原始件字节与上传的一模一样**
全量回归 370 条断言、0 失败(此时 provider=qiniu,文件真的进七牛)
顺带修掉一条"写死了存储位置"的 E2E 断言:e2e-m3-file.py 原来断言
「原始文件真的存在磁盘上」,切到七牛后整片变红,而那看起来像代码坏了。
改成通过产品接口把文件取回来比字节(GET /mp/files/{id}/url → 下载):
对两种 provider 都成立,而且比原来更强(顺带验了签名链接、私有访问、内容完整性)。
教训:**在"可替换的实现"上写断言,等于把测试绑死在一种实现上。**
状态:backend/application-local.yml 已是 provider: qiniu(密钥 gitignore、不进 jar);
生产用 XT_STORAGE_PROVIDER=qiniu + QINIU_ACCESS_KEY/QINIU_SECRET_KEY。
|
2026-09-29 14:52:51 +08:00 |
|
lovife
|
80a4050e53
|
feat(file): 材料上传与规范化(压缩到 1M 内 + 按规则改名)
这是产品的核心资产。实测那批真实材料里 **52.7% 的原始文件名是违规的**
(`曾伊豪-...110身份证.jpg.jpg` 这种),而认定机构只看两件事:
**体积有没有超 1M**、**名字对不对得上人**。这两件事做不对,
材料会被整批退回 —— 而退回时往往已经截止了。
两步走,不能合成一步:
① upload:先原样存下来。这一步必须快、必须成功 —— 学生的手机可能是慢网络,
上传中断是最常见的失败。先落盘,学生就不必重传。
② process:压缩 + 改名。慢(要解码、试多档质量),但在服务端跑,学生不用等网络。
压缩策略:**先保清晰度,再压质量,最后才缩尺寸**。顺序不能反 ——
证件类的可读性是底线(机构要能看清号码),缩过的证件翻拍件数字就糊了。
实测:11.6MB 的 2400x3200 原图 → **712KB**,1020x1360(P2 长边限 1600),q70。
命名策略:模板 + 白名单 + 非法字符清理。
⚠️ 非法字符清理不能省:学生的原始文件名里 `/` 很常见("学籍表(1/2)"),
而带 `/` 的名字**根本写不进磁盘** —— 表现是"保存失败",
而错误信息不会告诉你是因为文件名里有个斜杠。
原始文件**绝不覆盖**(另存 std/):认定机构退件时我们要能拿出原始件,
覆盖掉原图就再也说不清了("是不是你们压坏的")。E2E 断言了原图一字不差。
⚠️ 与契约的一处偏差(已知、有意):契约写的是 POST /mp/files/upload-token
(OSS 直传凭证),当前**后端代理接收**(multipart)。原因:后端还没接对象存储,
直传凭证要先有 bucket 与签名机制。硬做一个"返回假凭证"的接口只会让人以为能直传。
接入对象存储后可以并存:小文件走代理、大文件走直传。
⚠️ 当前把文件存在**本地磁盘**(fill.upload.dir)。多实例部署时各存各的会丢文件,
所以接入对象存储前**不能上多副本**。这一点必须记住,别当成"已经解决了"。
E2E 里修了一个夹具陷阱(值得记):用**裸 SQL** 插材料项会绕过 MaterialCatalog
的默认值(DDL 列默认是 AUTO / 1 份),于是"档位没生效""份数不对" ——
而这两条都会被误当成代码 bug。实测就踩了:IDCARD 期望 P2 + 2 份,
裸 SQL 插出来是 AUTO + 1 份,AUTO 的行为(不缩放、先降质量)被当成了"P2 没生效"。
细节修正:文件名里的脱敏号用 X 而不是下划线。`*` 在 Windows 文件名里非法,
被 sanitize 换成下划线后 `361029******0015` 变成 `361029_0015` ——
没人看得出那是脱敏号。现在是 `361029XXXXXX0015`。
验收:e2e-m3-file.py 29/29(含体积、档位、命名、原图保留、幂等、白名单、份数)。
|
2026-09-29 11:49:21 +08:00 |
|