fix(zip): UTF-8 zip 解码失败回退 GBK 漏抓 MalformedInputException + 邀请页守卫/称谓

后端 (3 个文件, 16+/5-):

* ocr/ZipExtractor.java: catch (IllegalArgumentException) →
  catch (IllegalArgumentException | MalformedInputException).
  ZipInputStream 用错 charset 解码 entry name 抛的是
  MalformedInputException (IOException 子类), 旧 catch 漏掉导致 GBK
  回退永远是死代码 (中文 Windows 打包的 zip 直接报 MalformedInputException
  上抛 500)。

* BizMeetingMaterialServiceImpl.java: 同款修复 (会务材料打包上传)

* BizMeetingAttendeeServiceImpl.java: 同款修复 (参会人批量导入 zip)

前端 (2 个文件, 6+/1-):

* MeetingDetail.vue pushInvite: posterUrl 空阻断 (无日程海报不允许
  发送邀请, 批量/行内共用入口, 一处守卫同时卡两路)

* MeetingInvitation.vue line 7: 称谓 "尊敬的 {{ name }}:" → "尊敬的
  {{ name }} 教授:" (医生点邀请函链接看到的网页)
This commit is contained in:
2026-09-16 04:30:51 +00:00
parent e8aed39675
commit 0553ca4fa3
5 changed files with 17 additions and 5 deletions
@@ -8,6 +8,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -42,12 +43,13 @@ public class ZipExtractor
log.info("zip 下载: url={} size={}B tmp={}", zipUrl, size, zipFile.getAbsolutePath());
// 优先 UTF-8 解包; 若 entry 名非 UTF-8 (常见 GBK, Windows 打包) 会抛
// IllegalArgumentException (malformed input) → 回退 GBK 重试
// MalformedInputException (IOException 子类, 不是 IllegalArgumentException)
// → 回退 GBK 重试
try
{
return doExtract(zipFile, tmpRoot, StandardCharsets.UTF_8);
}
catch (IllegalArgumentException e)
catch (IllegalArgumentException | MalformedInputException e)
{
log.warn("zip 文件名非 UTF-8, 回退 GBK 重试: {}", e.getMessage());
return doExtract(zipFile, tmpRoot, Charset.forName("GBK"));
@@ -7,6 +7,7 @@ import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.Charset;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -994,7 +995,9 @@ public class BizMeetingAttendeeServiceImpl implements IBizMeetingAttendeeService
private LinkedHashMap<String, ZipFolder> parseAttendeeZip(byte[] zipBytes) throws IOException {
try {
return readAttendeeZip(zipBytes, StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
} catch (IllegalArgumentException | MalformedInputException e) {
// 注: ZipInputStream 用错 charset 解码 entry name 抛 MalformedInputException (IOException 子类),
// 旧 catch (IllegalArgumentException) 漏掉, GBK 回退永远是死代码
log.warn("[attendee] zip UTF-8 解析失败, 回退 GBK: {}", e.getMessage());
return readAttendeeZip(zipBytes, Charset.forName("GBK"));
}
@@ -18,6 +18,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.MalformedInputException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.UUID;
@@ -841,8 +842,10 @@ public class BizMeetingMaterialServiceImpl implements IBizMeetingMaterialService
Map<String, List<String>> folderNames = new HashMap<>();
try {
readServiceZip(zipBytes, StandardCharsets.UTF_8, folderBytes, folderNames);
} catch (IllegalArgumentException e) {
} catch (IllegalArgumentException | MalformedInputException e) {
// 中文 Windows 打包器 (WinRAR/资源管理器) 用 GBK 编码条目名且不置 UTF-8 标志, 回退 GBK 重解析
// 注: ZipInputStream 用错 charset 解码 entry name 抛的是 MalformedInputException (IOException 子类),
// 不是 IllegalArgumentException, 旧 catch 漏掉导致 fallback 永远不会跑
log.warn("[material] 会务材料 zip UTF-8 解析失败, 回退 GBK: {}", e.getMessage());
folderBytes.clear();
folderNames.clear();
@@ -4,7 +4,7 @@
<div class="breadcrumb">首页 / 会议邀请函</div>
<!-- 称谓 -->
<p class="salutation">尊敬的 {{ attendeeName || '专家' }}</p>
<p class="salutation">尊敬的 {{ attendeeName || '专家' }} 教授</p>
<!-- 正文 -->
<p class="body-text">
@@ -1320,6 +1320,10 @@ function onBatchEsign() {
/** 调后端邀请参会 (推"会议邀请"站内信 + 置 is_invited=1, 不发短信); loadingId 非空→单条行级 loading, 否则批量工具栏 loading */
async function pushInvite(ids, loadingId) {
if (!ids || !ids.length) return
if (!row.value.posterUrl) {
ElMessage.warning('该会议未上传日程海报,不允许发送邀请。请先上传海报后再发送。')
return
}
if (loadingId != null) inviteLoadingId.value = loadingId
else inviteSending.value = true
try {