package com.ruoyi.project.common; import java.util.HashSet; import java.util.Set; import java.util.regex.Pattern; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; /** * 报销单附件文件名规范校验(2026-10-15)。 * *
业务口径(业务方确认): *
与前端 {@code opo-web/src/utils/fileNameCheck.js} 同口径: * 前端负责"上传即提醒 + 保存前拦截",本类负责后端保存时的二次校验(防止绕过前端)。 * *
用法(在保存入口里): *
* String err = AttachmentNameValidator.checkJsonList(detail.getInvoicefiles(), detail.getAnnexfiles());
* if (err != null) { return AjaxResult.error("附件文件名不符合要求,无法保存:" + err); }
*
*
* @author ruoyi
*/
public final class AttachmentNameValidator {
/** 主干(去扩展名)最大字符数 */
public static final int MAX_MAIN_LENGTH = 20;
public static final String MSG_NO_CHINESE = "为方便查验附件,请用中文规范命名附件!";
public static final String MSG_TOO_LONG = "为方便查验附件,附件名字长度不能超过20个字!";
public static final String MSG_ILLEGAL = "为方便查验附件,附件名不能包含空格及 / \\ : * ? \" < > | # % 等特殊字符!";
public static final String MSG_DUPLICATE = "为方便查验附件,同一行内不能有重名的附件!";
/** 附件项里取不到任何文件名(既没有 name 也没有 fileName)时的提示 */
public static final String MSG_NO_NAME = "为方便查验附件,附件必须带文件名,请重新上传!";
/** 汉字 */
private static final Pattern CHINESE = Pattern.compile("[\\u4e00-\\u9fa5]");
/** 非法字符:空格(含各类空白)+ / \ : * ? " < > | # % */
private static final Pattern ILLEGAL = Pattern.compile("[\\s/\\\\:*?\"<>|#%]");
private static final ObjectMapper MAPPER = new ObjectMapper();
private AttachmentNameValidator() {
}
/**
* 取主干(去掉最后一个扩展名)。
*/
public static String mainName(String fileName) {
String s = fileName == null ? "" : fileName.trim();
int index = s.lastIndexOf('.');
return index > 0 ? s.substring(0, index) : s;
}
/**
* 校验单个文件名。
*
* @param fileName 原始文件名
* @return {@code null}=通过;否则返回不合格原因
*/
public static String checkName(String fileName) {
String name = fileName == null ? "" : fileName.trim();
if (name.isEmpty()) {
return MSG_NO_CHINESE;
}
if (ILLEGAL.matcher(name).find()) {
return MSG_ILLEGAL;
}
String main = mainName(name);
if (!CHINESE.matcher(main).find()) {
return MSG_NO_CHINESE;
}
// 按字符数计(码点计数:中文/字母/数字/全角各计 1)
if (main.codePointCount(0, main.length()) > MAX_MAIN_LENGTH) {
return MSG_TOO_LONG;
}
return null;
}
/**
* 校验一个附件列表 JSON(形如 {@code [{"name":"张三发票.pdf","url":"/profile/upload/..."}]})。
*
* @param json 附件字段内容(annexfiles / invoicefiles),可为空
* @return {@code null}=通过;否则返回不合格原因(含文件名)
*/
public static String checkJson(String json) {
return checkJson(json, null, true);
}
/**
* 修改保存时使用:忽略"保存前已存在"的历史附件名。
*
* 业务口径(2026-10-15 确认):**只校验本次变更的附件**——历史单据里已存的不合规附件名,
* 在编辑该单据时不应把保存卡死,否则老单据改一个金额都存不进去。
*
* @param existingNames 保存前该明细已存在的附件名(用 {@link #collectNames(String)} 收集,
* 需为小写去空白形式;为空/为 null 表示按全新单据处理)
*/
public static String checkJsonForUpdate(String json, Set