yxh
3 天以前 3bde121db1c2a5fd0bc656c71e8fe6bdfde31074
修改了统计
已修改1个文件
已添加1个文件
278 ■■■■■ 文件已修改
ruoyi-project/src/main/java/com/ruoyi/project/common/AttachmentNameValidator.java 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/domain/SpStatCase.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-project/src/main/java/com/ruoyi/project/common/AttachmentNameValidator.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,246 @@
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)。
 *
 * <p>业务口径(业务方确认):
 * <ol>
 *   <li>必须包含中文(至少 1 ä¸ªæ±‰å­—)——"为方便查验附件,请用中文规范命名附件!"</li>
 *   <li>去掉扩展名后的主干,按字符数计算不能超过 20 ä¸ªå­———"为方便查验附件,附件名字长度不能超过20个字!"</li>
 *   <li>不能包含空格及 {@code / \ : * ? " < > | # %} ç­‰ç‰¹æ®Šå­—符</li>
 *   <li>同一明细行内不能重名(比较时忽略扩展名大小写与首尾空格)</li>
 * </ol>
 *
 * <p>与前端 {@code opo-web/src/utils/fileNameCheck.js} <b>同口径</b>:
 * å‰ç«¯è´Ÿè´£"上传即提醒 + ä¿å­˜å‰æ‹¦æˆª",本类负责后端保存时的<b>二次校验</b>(防止绕过前端)。
 *
 * <p>用法(在保存入口里):
 * <pre>
 *   String err = AttachmentNameValidator.checkJsonList(detail.getInvoicefiles(), detail.getAnnexfiles());
 *   if (err != null) { return AjaxResult.error("附件文件名不符合要求,无法保存:" + err); }
 * </pre>
 *
 * @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);
    }
    /**
     * ä¿®æ”¹ä¿å­˜æ—¶ä½¿ç”¨ï¼šå¿½ç•¥"保存前已存在"的历史附件名。
     *
     * <p>业务口径(2026-10-15 ç¡®è®¤ï¼‰ï¼š**只校验本次变更的附件**——历史单据里已存的不合规附件名,
     * åœ¨ç¼–辑该单据时不应把保存卡死,否则老单据改一个金额都存不进去。
     *
     * @param existingNames ä¿å­˜å‰è¯¥æ˜Žç»†å·²å­˜åœ¨çš„附件名(用 {@link #collectNames(String)} æ”¶é›†ï¼Œ
     *                      éœ€ä¸ºå°å†™åŽ»ç©ºç™½å½¢å¼ï¼›ä¸ºç©º/为 null è¡¨ç¤ºæŒ‰å…¨æ–°å•据处理)
     */
    public static String checkJsonForUpdate(String json, Set<String> existingNames) {
        return checkJson(json, existingNames, false);
    }
    /**
     * æ”¶é›†é™„ä»¶ JSON é‡Œå·²æœ‰çš„æ–‡ä»¶åï¼ˆå°å†™ã€åŽ»ç©ºç™½ï¼‰ï¼Œç”¨äºŽè¯†åˆ«"历史已存在"的附件。
     */
    public static Set<String> collectNames(String json) {
        Set<String> names = new HashSet<>();
        if (json == null || json.trim().isEmpty()) {
            return names;
        }
        try {
            JsonNode root = MAPPER.readTree(json);
            if (root != null && root.isArray()) {
                for (JsonNode node : root) {
                    String raw = fieldText(node, "name");
                    if (raw == null) {
                        raw = fieldText(node, "fileName");
                    }
                    if (raw != null && !raw.trim().isEmpty()) {
                        names.add(raw.trim().toLowerCase());
                    }
                }
            }
        } catch (Exception ignored) {
            // åŽ†å²å­—æ®µè§£æžå¤±è´¥å°±å½“ä½œ"没有历史附件",不因解析问题影响保存
        }
        return names;
    }
    /**
     * @param existingNames åŽ†å²é™„ä»¶åï¼ˆå°å†™ï¼‰ï¼Œå‘½ä¸­åˆ™è·³è¿‡æ ¡éªŒï¼›{@code null} è¡¨ç¤ºä¸å¿½ç•¥ä»»ä½•名字
     * @param strictNoName  {@code true}=取不到文件名时拦截(全新保存);{@code false}=放行(修改历史单据)
     */
    private static String checkJson(String json, Set<String> existingNames, boolean strictNoName) {
        if (json == null || json.trim().isEmpty()) {
            return null;
        }
        JsonNode root;
        try {
            root = MAPPER.readTree(json);
        } catch (Exception e) {
            // è§£æžä¸äº†å°±ä¸åœ¨è¿™é‡Œæ‹¦ï¼ˆä¿å­˜æµç¨‹è‡ªèº«ä¼šå¤„理格式问题),避免误拦
            return null;
        }
        if (root == null || !root.isArray()) {
            return null;
        }
        Set<String> seen = new HashSet<>();
        for (JsonNode node : root) {
            String raw = fieldText(node, "name");
            if (raw == null) {
                raw = fieldText(node, "fileName");
            }
            if (raw == null || raw.trim().isEmpty()) {
                // æ—¢æ²¡æœ‰ name ä¹Ÿæ²¡æœ‰ fileName:无法核对附件名。
                // å…¨æ–°ä¿å­˜ï¼ˆadd)按"不能保存"处理;修改历史单据时放行,避免卡住老数据
                if (strictNoName) {
                    return MSG_NO_NAME;
                }
                continue;
            }
            String name = raw.trim();
            if (isUploadPath(name)) {
                // è¯¥é™„件项存的是"上传后的存储路径"(形如 /profile/upload/2026/10/15/xxx.pdf),
                // ä¸æ˜¯æ“ä½œäººå‘½åçš„æ–‡ä»¶åï¼Œæ— æ³•据此判断命名是否规范 â†’ è·³è¿‡æ ¡éªŒï¼Œé¿å…è¯¯æ‹¦åˆæ³•单据。
                // å¾…前端改为上传时记录原始文件名(file.raw.name)后,这些项会自动纳入校验。
                continue;
            }
            if (existingNames != null && existingNames.contains(name.toLowerCase())) {
                // åŽ†å²é™„ä»¶ï¼ˆä¿å­˜å‰å·²å­˜åœ¨ï¼‰ä¸å‚ä¸Žæ ¡éªŒï¼šä¸šåŠ¡ç¡®è®¤"只校验本次变更的附件",避免卡住老单据
                continue;
            }
            String reason = checkName(name);
            if (reason != null) {
                return "【" + name + "】" + reason;
            }
            String key = name.toLowerCase();
            if (!seen.add(key)) {
                return "【" + name + "】" + MSG_DUPLICATE;
            }
        }
        return null;
    }
    /** è¯»å–附件项里的字符串字段,取不到(缺失/为空/null)返回 {@code null} */
    private static String fieldText(JsonNode node, String field) {
        if (node == null) {
            return null;
        }
        JsonNode value = node.get(field);
        if (value == null || value.isNull()) {
            return null;
        }
        String text = value.asText();
        return text == null || text.trim().isEmpty() ? null : text;
    }
    /** åˆ¤æ–­é™„件项里的 name æ˜¯å¦ä¸º"上传后的存储路径 / URL"(而不是操作人命名的文件名) */
    private static boolean isUploadPath(String name) {
        String lower = name.toLowerCase();
        return name.startsWith("/") || name.startsWith("http") || name.contains("/") || name.contains("\\")
                || lower.contains("profile/upload");
    }
    /**
     * æ ¡éªŒåŒä¸€æ˜Žç»†è¡Œä¸‹çš„多个附件列表(如发票 + å…¶å®ƒé™„件),逐个列表独立校验:
     * æ¯ä¸ªåˆ—表内部查重名(与前端 checkDetails çš„口径一致)。
     *
     * @param jsons é™„件字段内容,可为 null/空
     * @return {@code null}=通过;否则返回不合格原因
     */
    public static String checkJsonList(String... jsons) {        if (jsons == null) {
            return null;
        }
        for (String json : jsons) {
            String reason = checkJson(json);
            if (reason != null) {
                return reason;
            }
        }
        return null;
    }
    /** ä¿®æ”¹ä¿å­˜æ—¶ä½¿ç”¨ï¼šå¿½ç•¥åŽ†å²é™„ä»¶åï¼Œé€ä¸ªåˆ—è¡¨æ ¡éªŒï¼ˆä¾›"多个字段一起校验"的调用点) */
    public static String checkJsonListForUpdate(Set<String> existingNames, String... jsons) {
        if (jsons == null) {
            return null;
        }
        for (String json : jsons) {
            String reason = checkJsonForUpdate(json, existingNames);
            if (reason != null) {
                return reason;
            }
        }
        return null;
    }
}
ruoyi-project/src/main/java/com/ruoyi/project/domain/SpStatCase.java
@@ -24,13 +24,6 @@
    private static final long serialVersionUID = 1L;
    /**
     * donationmonth
     */
    @ApiModelProperty("donationmonth")
    @Excel(name = "统计月份", sort = 1)
    private String donationmonth;
    /**
     * deptid
     */
    @ApiModelProperty("deptid")
@@ -40,7 +33,7 @@
     * deptname
     */
    @ApiModelProperty("deptname")
    @Excel(name = "业务组", sort = 2)
    @Excel(name = "业务组", sort = 1)
    private String deptname;
    /**
@@ -53,90 +46,87 @@
     * reportername
     */
    @ApiModelProperty("reportername")
    @Excel(name = "专职人员", sort = 3)
    @Excel(name = "专职人员", sort = 2)
    private String reportername;
    /**
     * casesum
     */
    @ApiModelProperty("casesum")
    @Excel(name = "案例累计", sort = 4, cellType = Excel.ColumnType.NUMERIC)
    private String casesum;
    /**
     * çŽ¯èŠ‚ 0:潜在捐献
     */
    @ApiModelProperty("潜在捐献")
    @Excel(name = "潜在捐献", sort = 5, cellType = Excel.ColumnType.NUMERIC)
    private String workflow0;
    /**
     * çŽ¯èŠ‚ 1:医学评估
     */
    @ApiModelProperty("医学评估")
    @Excel(name = "医学评估", sort = 6, cellType = Excel.ColumnType.NUMERIC)
    private String workflow1;
    /**
     * çŽ¯èŠ‚ 2:家属确认
     */
    @ApiModelProperty("家属确认")
    @Excel(name = "家属确认", sort = 7, cellType = Excel.ColumnType.NUMERIC)
    private String workflow2;
    /**
     * çŽ¯èŠ‚ 3:伦理审查
     */
    @ApiModelProperty("伦理审查")
    @Excel(name = "伦理审查", sort = 8, cellType = Excel.ColumnType.NUMERIC)
    private String workflow3;
    /**
     * çŽ¯èŠ‚ 4:器官分配
     */
    @ApiModelProperty("器官分配")
    @Excel(name = "器官分配", sort = 9, cellType = Excel.ColumnType.NUMERIC)
    private String workflow4;
    /**
     * çŽ¯èŠ‚ 5:获取见证
     */
    @ApiModelProperty("获取见证")
    @Excel(name = "获取见证", sort = 10, cellType = Excel.ColumnType.NUMERIC)
    private String workflow5;
    /**
     * çŽ¯èŠ‚ 6:捐献完成
     */
    @ApiModelProperty("捐献完成")
    @Excel(name = "捐献完成", sort = 11, cellType = Excel.ColumnType.NUMERIC)
    private String workflow6;
    /**
     * çŽ¯èŠ‚ 7:归档撤销
     */
    @ApiModelProperty("归档撤销")
    @Excel(name = "归档撤销", sort = 12, cellType = Excel.ColumnType.NUMERIC)
    private String workflow7;
    /**
     * çŽ¯èŠ‚ 8:案例归档
     */
    @ApiModelProperty("案例归档")
    @Excel(name = "案例归档", sort = 13, cellType = Excel.ColumnType.NUMERIC)
    private String workflow8;
    /**
     * çŽ¯èŠ‚ 9:案例终止(页面已隐藏该列,导出也不输出)
     * çŽ¯èŠ‚ 9:案例终止
     */
    @ApiModelProperty("案例终止")
    private String workflow9;
    /**
     * æ¡ˆä¾‹ç´¯è®¡ï¼ˆworkflow 7 æˆ– 8)
     */
    @ApiModelProperty("案例累计")
    @Excel(name = "案例累计", sort = 3, cellType = Excel.ColumnType.NUMERIC)
    private String workflowsum;
    /**
     * ç»ˆæ­¢æ¡ˆä¾‹æ•°ï¼ˆtermination_case=1)
     */
    @ApiModelProperty("终止案例数")
    @Excel(name = "终止案例", sort = 14, cellType = Excel.ColumnType.NUMERIC)
    @Excel(name = "终止案例", sort = 4, cellType = Excel.ColumnType.NUMERIC)
    private String terminationcase;
}