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:
+37
-2
@@ -184,6 +184,25 @@ def _portrait_phone_jpeg(w: int = 3200, h: int = 2400) -> bytes:
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def make_png(w: int = 900, h: int = 1200) -> bytes:
|
||||
"""
|
||||
造一张 PNG(截图类材料的常见形态)。
|
||||
|
||||
用来验证"格式按**字节**判、不按后缀判":同一份字节改个后缀名,
|
||||
服务端必须仍然认出它是 png。
|
||||
"""
|
||||
from PIL import Image
|
||||
img = Image.new("RGB", (w, h), (247, 247, 247))
|
||||
px = img.load()
|
||||
# 画几行横线,模拟表格(也让它不是纯色,免得压出来只有几百字节)
|
||||
for y in range(40, h, 60):
|
||||
for x in range(30, w - 30):
|
||||
px[x, y] = (30, 30, 30)
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, "PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def make_big_photo(w: int = 2400, h: int = 3200) -> bytes:
|
||||
"""造一张"大且难压"的图:随机噪点。纯色图会被压到几十 KB,测不出压缩能力。"""
|
||||
from PIL import Image
|
||||
@@ -393,8 +412,24 @@ def main() -> int:
|
||||
st3 == 200 and b"Exif\x00\x00" not in body3[:4096],
|
||||
f"HTTP {st3},前 4096 字节里{'有' if b'Exif' in body3[:4096] else '没有'} EXIF")
|
||||
|
||||
# ── 8. 清理 ──────────────────────────────────────
|
||||
print("\n[8] 清理")
|
||||
# ── 8. 客户端压缩后的上传(文件名没有后缀 / 格式按字节判)─────────
|
||||
print("\n[8] 客户端压缩后上传:文件名没后缀也要收(真 bug 的回归)")
|
||||
# 客户端压缩完交给 uni.uploadFile 的是 blob 地址,multipart 里的名字是一串 UUID、
|
||||
# **没有后缀** —— 而修复前服务端只看后缀,会把一张好端端的照片判成 .bin 拒掉。
|
||||
# 也就是"前端压缩一上线,学生就传不了材料"。
|
||||
sql(f"UPDATE collect_file SET deleted = 1 WHERE submission_id={sid} AND material_code='IDCARD'")
|
||||
r = upload("/api/v1/mp/files/upload", tok, sid, "IDCARD",
|
||||
"4c1e2f3a-0000-1111-2222-333344445555", raw[:60000], "image/jpeg")
|
||||
check("**没有后缀**的真 JPEG 被接受(按字节判格式,不看后缀)", r.get("code") == 0, f"{r}")
|
||||
|
||||
# 逆过来:字节是真 PNG、但后缀写着 .txt —— 按字节认作 png(在白名单里)就该收。
|
||||
# 守的是"学生把截图改名成 .txt 交上来",别等导出给机构才发现格式不对。
|
||||
png = make_png(900, 1200)
|
||||
r2 = upload("/api/v1/mp/files/upload", tok, sid, "IDCARD", "身份证.txt", png, "text/plain")
|
||||
check("字节是 PNG、后缀写着 .txt → 按字节认作 png 并接受", r2.get("code") == 0, f"{r2}")
|
||||
|
||||
# ── 9. 清理 ──────────────────────────────────────
|
||||
print("\n[9] 清理")
|
||||
sql(f"DELETE FROM collect_file WHERE submission_id IN ({','.join(str(x) for x in CREATED_SUBS)})")
|
||||
sql(f"DELETE FROM collect_submission WHERE id IN ({','.join(str(x) for x in CREATED_SUBS)})")
|
||||
sql(f"UPDATE collect_roster SET bind_user_id=NULL, bind_time=NULL, bind_type=NULL WHERE id={rid}")
|
||||
|
||||
Reference in New Issue
Block a user