feat(mp): 前端压缩做完 —— 上传体积降到 1/8,顺带修掉两个真 bug
一、效果(浏览器实测,真存储、真接口)
手机竖拍 3200×2400 + EXIF Orientation=6,3699KB
→ 1200×1600(竖的,转正正确)· 438KB · q88 · 73ms
服务端**原样收下**:collect_file 里 raw_size=448940、final_size=448940、
quality=0(NOT_REENCODED)—— 字节完全相同,没有二次编码。
二、🐛 bug 一:浏览器对 EXIF 的处理**不一致**,猜必错一半
写完前端旋正后实测,输出是 1600×1200(横的)—— 又躺倒了。量出来:
文件里的像素 3200×2400(横)+ Orientation=6
img.naturalWidth/Height 2400×3200 ← 浏览器**已经自己按 EXIF 转正了**
createImageBitmap('none') 2400×3200 ← 连明确要求不给转也被忽略
现代浏览器会把 EXIF 先转正再交给 drawImage,我又转一次 → 转两下 = 又躺倒;
而老实现(旧 X5 / 旧 WKWebView)给的是原始像素,不转就是躺倒。
两端行为相反,**任何写死的做法都必错一半**。
解法:不猜,量出来再决定 —— 自己从 JPEG 的 SOF 段读**文件里的原始像素**
(readJpegSize,与 EXIF 无关),再和 naturalWidth/Height 比:
· 与原始尺寸互换 → 浏览器已经转过 → 不要再转
· 两者相同 → 给的是原始像素 → 我们自己转
· 都对不上 → 按现代浏览器的默认(已转正)处理
这和第 §一·二十 的 EXIF bug 是同一件事的两面:
后端不转是躺倒,前端多转一次也是躺倒。
三、🐛 bug 二:按后缀判格式 → 前端压缩一上线,学生就传不了材料
上传被拒:「身份证」只接受 jpg/jpeg/png,你传的是 .bin。
H5 用 **blob 地址**调 uni.uploadFile,multipart 里的文件名是一串 UUID、
**没有后缀**,而后端当时按 extOf(name) 判白名单 → 判成 .bin → 拒。
这不是脚手架的问题:前端压缩产出的就是 blob 地址,一上线所有压缩过的照片
都会被拒。两处一起修:
· 后端改为按**字节幻数**判格式(sniffExt),幻数认不出再退回看后缀
(顺带变强:PNG 改名成 .jpg 也能正确识别)
· 接口新增可选 fileName 字段,客户端显式声明文件名;
original_name 只用于展示留痕、不进路径拼接,并截断防超长
四、档位 → 前端行为的映射(**不按材料码猜**,档位是管理员可改的配置)
P2 证件翻拍 → 压:长边 1600 + q88 起阶梯
P3 扫描件 → 压:长边 3508 + 降质量(别套 P2 的 1600,会看不清字)
AUTO/P5 → 只降质量不缩尺寸(未知档位擅自缩尺寸是危险的)
P1 证件照 → 不压(服务端要 295×413 裁切 + 补白底)
P4 截图 → 不压(关键是保持 PNG,canvas 只能出 JPEG 会糊掉表格线)
与服务端配合的关键:必须压成**正好落在服务端那套目标里** ——
压狠了白掉一次画质,压轻了服务端再压一次、前端白干(实测 quality=0 证明走对了)。
五、小程序端:**刻意不做 canvas 旋正**(真机未验证)
orientation === 'up' → compressImage 压;其它朝向 → 不压、直传,交服务端转正。
理由:小程序 canvas 的旋正在真机上无法验证,转错了的后果是机构收到躺倒的身份证;
宁可慢一点,也不能赌。
六、E2E
e2e-m3-file 39/39,新增第 8 节守这两个 bug:
✓ **没有后缀**的真 JPEG 被接受(按字节判格式,不看后缀)
✓ 字节是 PNG、后缀写着 .txt → 按字节认作 png 并接受
全量 469 条断言、0 失败。
七、验证脚手架已拆
pages/dev/compress-check.vue + static/__devcheck/ + 造图脚本都是一次性的,
已全部删除并重建 H5(自查过产物里不再有 compress-check / __devcheck)。
它的价值在于:文件选择框没法自动化(chooseImage 会弹系统对话框),
所以那页绕开"选文件",直接拿真构造的竖拍照片喂给**真正的压缩模块**、
走**真正的上传通道** —— 验的比"尺寸对不对"更硬:
**定位色块落在右上角**(证明转的是 90° 顺时针,不只是宽高换了位置)。
This commit is contained in:
+136
@@ -1492,3 +1492,139 @@ m1-materials 68 / m1-form 48 / m1-task 71 / m1-orgs 48
|
||||
`@"..."@` 会把 `"` 吃掉,于是 `return "bin";` 变成 `return bin;`、
|
||||
`throw new BizException(..., "文案")` 变成语法错误。
|
||||
本轮为这个坑白花了三次编译。**带引号的代码一律用 edit 工具改,不要过转义层。**
|
||||
|
||||
---
|
||||
|
||||
## §一·二十一 前端压缩做完了 —— 顺带又抓到两个真 bug
|
||||
|
||||
### 效果(浏览器实测,真存储、真接口)
|
||||
|
||||
```
|
||||
输入:手机竖拍 3200×2400(像素是横的)+ EXIF Orientation=6,3699KB
|
||||
输出:1200×1600(竖的,转正正确)· 438KB · q88 · 73ms
|
||||
—— 上传体积降到 1/8,服务端**原样收下**(quality=0 NOT_REENCODED)
|
||||
```
|
||||
|
||||
数据库里那条权威记录(`collect_file`):
|
||||
```
|
||||
original_name 压缩测试.jpg ← 客户端声明的文件名生效
|
||||
standard_name 张同学-361029XXXXXX0015-身份证.jpg
|
||||
raw_size 448940 ← 上传上来的
|
||||
final_size 448940 ← 存下来的,**字节完全相同** → 服务端没有二次编码
|
||||
width×height 1200×1600
|
||||
quality 0 ← NOT_REENCODED
|
||||
```
|
||||
|
||||
### 🐛 bug 一:浏览器对 EXIF 的处理**不一致**,猜必错一半
|
||||
|
||||
写完前端旋正后实测,输出是 **1600×1200(横的)** —— 又躺倒了。量出来的原因是:
|
||||
|
||||
```
|
||||
文件里的像素 3200×2400(横)+ Orientation=6
|
||||
img.naturalWidth/Height 2400×3200 ← 浏览器**已经自己按 EXIF 转正了**
|
||||
createImageBitmap('none') 2400×3200 ← 连"明确要求不给转"也被忽略
|
||||
```
|
||||
|
||||
也就是说:**现代浏览器(Chromium/WebKit)会把 EXIF 先转正再交给 `drawImage`**,
|
||||
我又转了一次 → 转了两下 = 又躺倒了。而**老实现(旧 X5 / 旧 WKWebView)给的是原始像素**,
|
||||
不转就是躺倒的。两端行为相反,**任何写死的做法都必错一半**。
|
||||
|
||||
解法:**不猜,量出来再决定**(`utils/image-compress.ts`)——
|
||||
自己从 JPEG 的 SOF 段读**文件里的原始像素**(`readJpegSize`,与 EXIF 无关),
|
||||
再和 `naturalWidth/Height` 比:
|
||||
- 与原始尺寸**互换** → 浏览器已经转过了 → **不要再转**
|
||||
- 两者**相同** → 给的是原始像素 → 我们自己转
|
||||
- 都对不上(非 JPEG 等)→ 按现代浏览器的默认(已转正)处理
|
||||
|
||||
> 正方形图两种情况分辨不出来(此时按"已转正"处理)——现代浏览器下这是对的,
|
||||
> 而正方形带朝向标记的图本身极罕见(一般是裁剪图)。这条已知边界写进注释了。
|
||||
|
||||
**这是这轮最有价值的一条**:它和第 §一·二十 的 EXIF bug 是同一件事的两面 ——
|
||||
后端不转是躺倒,前端多转一次也是躺倒。
|
||||
|
||||
### 🐛 bug 二:按文件名的后缀判格式 → 前端压缩一上线,学生就传不了材料
|
||||
|
||||
浏览器验证时上传被拒:**「身份证」只接受 jpg/jpeg/png,你传的是 .bin」**。
|
||||
|
||||
原因:H5 用 **blob 地址**调 `uni.uploadFile`,multipart 里带过去的文件名是一串 UUID、
|
||||
**没有后缀**;而后端当时是 `extOf(originalName)` 判白名单 → 判成 `.bin` → 拒。
|
||||
|
||||
**这不是脚手架的问题,是真 bug**:前端压缩产出的就是 blob 地址,
|
||||
一上线所有压缩过的照片都会被拒 —— 也就是"学生压根传不了材料"。
|
||||
|
||||
两处一起修:
|
||||
- **后端**(`MaterialFileService.upload`):格式按**字节幻数**判(`sniffExt`),
|
||||
不信后缀;幻数认不出来(HEIC 等)再退回看后缀,保持原有行为。
|
||||
> 顺带变强了:学生把 PNG 改名成 `.jpg` 交上来,现在能按 PNG 正确识别
|
||||
> (白名单里 png 允许,所以收下;原来是"看着是 jpg 就收")。
|
||||
- **接口**:新增可选 `fileName` 表单字段,客户端显式声明叫什么;
|
||||
`original_name` 只用于展示与留痕,**不进任何路径拼接**,并截断防超长。
|
||||
|
||||
### 档位 → 前端行为的映射(**不按材料码猜**)
|
||||
|
||||
档位来自后端配置(`collect_material_def.compress_profile`,管理员可改),
|
||||
前端按档位推导,材料码一个都不写死 —— 否则管理员一改配置前端就错,而且错得悄无声息。
|
||||
|
||||
| 档位 | 前端行为 | 为什么 |
|
||||
|---|---|---|
|
||||
| **P2**(证件翻拍) | 压:长边 1600 + q88 起阶梯 | 与服务端 P2 同口径,压完正好被服务端原样收下 |
|
||||
| **P3**(扫描件) | 压:长边 3508 + 降质量 | 长边上限很宽(A4@300dpi),收益主要在降质量;**千万别套 P2 的 1600**,扫描件缩到 1600 就看不清字 |
|
||||
| **AUTO/P5** | 只降质量,**不缩尺寸** | 与服务端同口径;未知档位下擅自缩尺寸是危险的(万一是文档类) |
|
||||
| **P1**(证件照) | **不压**,直传 | 服务端要按 295×413 裁切 + **补白底**,前端做不了;硬压只会让裁出来的头像更糊 |
|
||||
| **P4**(截图/文档) | **不压**,直传 | 关键是**保持 PNG**(转 JPG 会把表格线糊掉),而 canvas 只能出 JPEG |
|
||||
|
||||
### 小程序端(**刻意不做 canvas 旋正**,真机未验证)
|
||||
|
||||
- `getImageInfo().orientation === 'up'` → `compressImage` 压(无需转正,重编码不改朝向)
|
||||
- **其它朝向 → 不压,直传原图,交服务端**(服务端已经会按 EXIF 正确转正)
|
||||
|
||||
理由:小程序 canvas 的旋正行为**我在真机上无法验证**,而"转错了"的后果是
|
||||
机构收到躺倒的身份证。宁可慢一点,也不能赌。等有真机环境再补。
|
||||
|
||||
### 与服务端配合的关键点(决定了参数怎么取)
|
||||
|
||||
服务端会判断"是不是已经达标"(没触发缩放 && 没有 EXIF 需转正 && 体积在上限内)→
|
||||
**原样收下不重编码**。所以前端必须压成**正好落在服务端那套目标里**:
|
||||
|
||||
- 压得比服务端狠 → 白掉一次画质
|
||||
- 压得比服务端轻 → 服务端再压一次,前端白干
|
||||
|
||||
实测已确认落在这条路上(`quality=0`)。
|
||||
|
||||
### E2E
|
||||
|
||||
- `e2e-m3-file.py` **39/39**,新增第 8 节守这次的两个 bug:
|
||||
```
|
||||
✓ **没有后缀**的真 JPEG 被接受(按字节判格式,不看后缀)
|
||||
✓ 字节是 PNG、后缀写着 .txt → 按字节认作 png 并接受
|
||||
```
|
||||
(原有的"`身份证.exe` + `MZfake` 字节被拒"仍通过:幻数认不出 → 退回看后缀 → 拒)
|
||||
- **全量 469 条断言、0 失败**
|
||||
|
||||
### 验证脚手架(已拆,别留在仓库里)
|
||||
|
||||
`pages/dev/compress-check.vue` + `static/__devcheck/exif6-portrait.jpg` + 造图脚本
|
||||
是一次性的浏览器端验证脚手架 —— **已全部删除**,H5 产物也重新构建过
|
||||
(自查过产物里不再有 `compress-check` / `__devcheck` 字样)。
|
||||
|
||||
它当时的作用值得记一笔:**文件选择框没法自动化**(`uni.chooseImage` 会弹系统对话框),
|
||||
所以那页绕开"选文件",直接拿一张真构造的竖拍照片喂给**真正的压缩模块**、
|
||||
再走**真正的上传通道** —— 除了"手指点选"那一步,其余全是生产代码。
|
||||
它验的东西比"尺寸对不对"更硬:**定位色块落在右上角**(证明转的是 90° 顺时针,
|
||||
而不只是"宽高换了个位置")。
|
||||
|
||||
### ⏳ 还没做 / 待办
|
||||
|
||||
- **小程序端真机验证**(`compressImage` 那条路;顺带确认 `getImageInfo().orientation` 的实际取值)
|
||||
- 七牛**实时样式做缩略图**(审核列表 200 张 × 700KB ≈ 140MB → 缩略图 ≈ 2MB)
|
||||
- D6 微信三通道体积上限真机实测(现在前端压过了,这条的紧迫性下降了)
|
||||
|
||||
### 流程备注(本轮又踩的)
|
||||
|
||||
- **PowerShell 不支持 heredoc**(`python - <<'PY'`)—— 会报
|
||||
"Missing file specification after redirection operator"。改中文文档/改代码
|
||||
一律写成 `.py` 脚本文件再跑,别在 shell 里内联。
|
||||
- 浏览器验证可以用 `browser_evaluate` 读页面状态 + `browser_wait(condition:'textGone')`
|
||||
等结果,比"睡一会儿再看"可靠;`browser.navigate` 带个 `?v=N` 就能绕过缓存拿新产物。
|
||||
- 改完 `pages.json` **别忘重建 H5**:产物里的旧页面不会自己消失
|
||||
(这轮就出现过"临时页文件删了、`pages.json` 还留着条目 → 构建直接失败")。
|
||||
|
||||
Reference in New Issue
Block a user