Ver Fonte

前缀白名单

wangxichen há 2 dias atrás
pai
commit
3b9b9dfebc

+ 98 - 0
src/com/mes/core/ProjectConfigManager.java

@@ -106,6 +106,10 @@ public class ProjectConfigManager {
     private String  deviceRow2LabelOverride;
     // 物料码输入框显隐覆盖
     private Boolean showMaterialInputOverride;
+    // 前缀白名单:用户在"设置→前缀白名单"里追加的前缀(内置项目前缀不在此列,不可删)
+    private final List<String> extraProductPrefixes = new ArrayList<>();
+    private final List<String> extraColdPlatePrefixes = new ArrayList<>();
+    private final List<String> extraBottomPlatePrefixes = new ArrayList<>();
     private final List<Listener> listeners = new ArrayList<>();
 
     private ProjectConfigManager() {
@@ -169,6 +173,13 @@ public class ProjectConfigManager {
                 deviceRow2LabelOverride = trimToNull(uiObj.getString("deviceRow2Label"));
                 showMaterialInputOverride = uiObj.getBoolean("showMaterialInput");
             }
+            // 前缀白名单(用户追加部分)
+            JSONObject wlObj = json.getJSONObject("prefixWhitelist");
+            if (wlObj != null) {
+                loadPrefixList(wlObj, "product", extraProductPrefixes);
+                loadPrefixList(wlObj, "coldPlate", extraColdPlatePrefixes);
+                loadPrefixList(wlObj, "bottomPlate", extraBottomPlatePrefixes);
+            }
             log.info("[ProjectConfig] 加载成功: project={}, ip={}, station={}, lineSn={}, features={}",
                     currentProjectId, deviceIp, stationCodeOverride, lineSnOverride, featuresOverride);
         } catch (Exception e) {
@@ -198,6 +209,11 @@ public class ProjectConfigManager {
             if (!featuresOverride.isEmpty()) {
                 root.put("featuresOverride", featuresOverride);
             }
+            JSONObject wlObj = new JSONObject();
+            if (!extraProductPrefixes.isEmpty())     wlObj.put("product", extraProductPrefixes);
+            if (!extraColdPlatePrefixes.isEmpty())   wlObj.put("coldPlate", extraColdPlatePrefixes);
+            if (!extraBottomPlatePrefixes.isEmpty()) wlObj.put("bottomPlate", extraBottomPlatePrefixes);
+            if (!wlObj.isEmpty()) root.put("prefixWhitelist", wlObj);
             root.put("lastModified", System.currentTimeMillis());
             String content = JSON.toJSONString(root, JSONWriter.Feature.PrettyFormat);
             Files.write(configPath, content.getBytes(StandardCharsets.UTF_8));
@@ -261,6 +277,88 @@ public class ProjectConfigManager {
         return null;
     }
 
+    // ========== 前缀白名单(内置项目前缀 + 用户追加) ==========
+
+    /** 白名单类别 */
+    public enum PrefixKind { PRODUCT, COLD_PLATE, BOTTOM_PLATE }
+
+    private static void loadPrefixList(JSONObject obj, String key, List<String> target) {
+        target.clear();
+        List<String> arr = obj.getList(key, String.class);
+        if (arr == null) return;
+        for (String s : arr) {
+            if (s == null) continue;
+            String t = s.trim();
+            if (!t.isEmpty() && !target.contains(t)) target.add(t);
+        }
+    }
+
+    private List<String> extraListOf(PrefixKind kind) {
+        switch (kind) {
+            case COLD_PLATE:   return extraColdPlatePrefixes;
+            case BOTTOM_PLATE: return extraBottomPlatePrefixes;
+            default:           return extraProductPrefixes;
+        }
+    }
+
+    /** 当前项目的内置前缀(只读,UI 中不可删除),无则返回空串 */
+    public String getBuiltinPrefix(PrefixKind kind) {
+        String p;
+        switch (kind) {
+            case COLD_PLATE:   p = getColdPlatePrefix(); break;
+            case BOTTOM_PLATE: p = getBottomPlatePrefix(); break;
+            default:           p = getProductPrefix(); break;
+        }
+        return p == null ? "" : p.trim();
+    }
+
+    /** 用户追加的前缀(可删除) */
+    public List<String> getExtraPrefixes(PrefixKind kind) {
+        return new ArrayList<>(extraListOf(kind));
+    }
+
+    /**
+     * 覆盖某一类别的用户追加前缀(内置前缀不受影响)
+     */
+    public void setExtraPrefixes(PrefixKind kind, List<String> prefixes) {
+        List<String> target = extraListOf(kind);
+        String builtin = getBuiltinPrefix(kind);
+        target.clear();
+        if (prefixes != null) {
+            for (String s : prefixes) {
+                if (s == null) continue;
+                String t = s.trim();
+                // 与内置重复的不再单独存一份
+                if (t.isEmpty() || t.equals(builtin) || target.contains(t)) continue;
+                target.add(t);
+            }
+        }
+        save();
+        log.info("[ProjectConfig] 更新{}白名单: {}", kind, target);
+    }
+
+    /**
+     * 生效的前缀列表 = 内置前缀(非空时)+ 用户追加
+     * 返回空列表表示该类别不做前缀校验
+     */
+    public List<String> getEffectivePrefixes(PrefixKind kind) {
+        List<String> result = new ArrayList<>();
+        String builtin = getBuiltinPrefix(kind);
+        if (!builtin.isEmpty()) result.add(builtin);
+        for (String s : extraListOf(kind)) {
+            if (!result.contains(s)) result.add(s);
+        }
+        return result;
+    }
+
+    /** 按物料标签取生效前缀列表,标签无法识别时返回 null(交由调用方回退 yaml) */
+    public List<String> getEffectiveMaterialPrefixesByLabel(String label) {
+        if (label == null) return null;
+        if (label.contains("冷板")) return getEffectivePrefixes(PrefixKind.COLD_PLATE);
+        if (label.contains("底护板")) return getEffectivePrefixes(PrefixKind.BOTTOM_PLATE);
+        return null;
+    }
+
     // ========== 切换项目 ==========
 
     public void switchProject(String projectId) {

+ 13 - 0
src/com/mes/step/AbstractStep.java

@@ -219,4 +219,17 @@ public abstract class AbstractStep implements IWorkflowStep {
             asyncCompleteCallback.onAsyncComplete(success);
         }
     }
+
+    /**
+     * 前缀白名单校验:命中任意一个前缀即通过
+     * 列表为空表示不校验,直接放行
+     */
+    protected static boolean matchesAnyPrefix(String code, java.util.List<String> prefixes) {
+        if (prefixes == null || prefixes.isEmpty()) return true;
+        if (code == null) return false;
+        for (String p : prefixes) {
+            if (p != null && !p.isEmpty() && code.startsWith(p)) return true;
+        }
+        return false;
+    }
 }

+ 8 - 7
src/com/mes/step/ScanMaterialStep.java

@@ -60,14 +60,15 @@ public class ScanMaterialStep extends AbstractStep {
         // 用户扫码后:验证物料码
         String materialSn = context.getMaterialSn().trim();
 
-        // 前缀校验:优先按 label 从项目配置读取,fallback 到 yaml
-        String effectivePrefix = ProjectConfigManager.getInstance().getMaterialPrefixByLabel(label);
-        if (effectivePrefix == null || effectivePrefix.isEmpty()) {
-            effectivePrefix = prefix;
+        // 前缀校验:按 label 取白名单(内置项目前缀+用户追加),为空则回退 yaml
+        java.util.List<String> allowed = ProjectConfigManager.getInstance()
+                .getEffectiveMaterialPrefixesByLabel(label);
+        if ((allowed == null || allowed.isEmpty()) && prefix != null && !prefix.isEmpty()) {
+            allowed = java.util.Collections.singletonList(prefix);
         }
-        if (effectivePrefix != null && !effectivePrefix.isEmpty() && !materialSn.startsWith(effectivePrefix)) {
-            log.warn("[{}] {}前缀错误: {},期望前缀: {}", context.getStationCode(), label, materialSn, effectivePrefix);
-            context.setStatusMessage(label + "前缀应为 " + effectivePrefix, -1);
+        if (allowed != null && !allowed.isEmpty() && !matchesAnyPrefix(materialSn, allowed)) {
+            log.warn("[{}] {}前缀错误: {},允许前缀: {}", context.getStationCode(), label, materialSn, allowed);
+            context.setStatusMessage(label + "前缀应为 " + String.join(" / ", allowed), -1);
             context.setMaterialSn(null);
 
             // 格式错误,重新弹出扫码框

+ 8 - 7
src/com/mes/step/ScanProductStep.java

@@ -42,14 +42,15 @@ public class ScanProductStep extends AbstractStep {
             log.info("[{}] 36位码处理: {} -> {}", context.getStationCode(), productSn, processedSn);
         }
 
-        // 前缀校验:优先读项目配置,fallback 到 yaml
-        String effectivePrefix = ProjectConfigManager.getInstance().getProductPrefix();
-        if (effectivePrefix == null || effectivePrefix.isEmpty()) {
-            effectivePrefix = prefix;
+        // 前缀校验:白名单(内置项目前缀+用户追加),为空则回退 yaml
+        java.util.List<String> allowed = ProjectConfigManager.getInstance()
+                .getEffectivePrefixes(ProjectConfigManager.PrefixKind.PRODUCT);
+        if (allowed.isEmpty() && prefix != null && !prefix.isEmpty()) {
+            allowed = java.util.Collections.singletonList(prefix);
         }
-        if (effectivePrefix != null && !effectivePrefix.isEmpty() && !processedSn.startsWith(effectivePrefix)) {
-            log.warn("[{}] 工件码前缀错误: {},期望前缀: {}", context.getStationCode(), processedSn, effectivePrefix);
-            context.setStatusMessage("工件码前缀应为 " + effectivePrefix, -1);
+        if (!allowed.isEmpty() && !matchesAnyPrefix(processedSn, allowed)) {
+            log.warn("[{}] 工件码前缀错误: {},允许前缀: {}", context.getStationCode(), processedSn, allowed);
+            context.setStatusMessage("工件码前缀应为 " + String.join(" / ", allowed), -1);
             context.setProductSn(null);  // 清空,让用户重新扫
             return false;
         }

+ 156 - 0
src/com/mes/ui/MainFrame.java

@@ -86,6 +86,7 @@ public class MainFrame extends JFrame {
     private JMenuItem serverIpItem;
     private JMenuItem featuresItem;
     private JMenuItem deviceInfoUiItem;
+    private JMenuItem prefixWhitelistItem;
     // 界面布局子菜单(mes123解锁后显示)
     private JMenu layoutMenu;
     // 手动提交子菜单(解锁后显示;若勾选"始终显示"则不受解锁控制)
@@ -273,6 +274,12 @@ public class MainFrame extends JFrame {
         deviceInfoUiItem.addActionListener(e -> showDeviceInfoUiDialog());
         projectDeviceMenu.add(deviceInfoUiItem);
 
+        prefixWhitelistItem = new JMenuItem("前缀白名单");
+        prefixWhitelistItem.setIcon(new ImageIcon(Objects.requireNonNull(getClass().getResource("/resources/image/bg/menu_setting.png"))));
+        prefixWhitelistItem.setFont(new Font("微软雅黑", Font.PLAIN, 20));
+        prefixWhitelistItem.addActionListener(e -> showPrefixWhitelistDialog());
+        projectDeviceMenu.add(prefixWhitelistItem);
+
         refreshMenuTexts();  // 首次填充显示当前值
 
         // ========== 子菜单:界面布局(默认隐藏,密码解锁后显示) ==========
@@ -1411,6 +1418,155 @@ public class MainFrame extends JFrame {
                 "切换成功", JOptionPane.INFORMATION_MESSAGE);
     }
 
+    // ========== 前缀白名单 ==========
+
+    /** 内置前缀在列表中的标记后缀,带此后缀的项不可删除 */
+    private static final String BUILTIN_TAG = "  (内置)";
+
+    /**
+     * 前缀白名单里的一个类别(工件码/冷板码/底护板码)
+     * 内置前缀置顶回显且不可删除,用户追加项可增删
+     */
+    private static final class PrefixSection {
+        private final com.mes.core.ProjectConfigManager.PrefixKind kind;
+        private final String builtin;
+        private final DefaultListModel<String> model = new DefaultListModel<>();
+        private final JPanel panel = new JPanel(new BorderLayout(4, 4));
+
+        PrefixSection(String title,
+                      com.mes.core.ProjectConfigManager.PrefixKind kind,
+                      com.mes.core.ProjectConfigManager pc,
+                      Component owner) {
+            this.kind = kind;
+            this.builtin = pc.getBuiltinPrefix(kind);
+
+            if (!builtin.isEmpty()) model.addElement(builtin + BUILTIN_TAG);
+            for (String s : pc.getExtraPrefixes(kind)) model.addElement(s);
+
+            JList<String> list = new JList<>(model);
+            list.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+            JScrollPane sp = new JScrollPane(list);
+            sp.setPreferredSize(new Dimension(190, 150));
+
+            JButton addBtn = new JButton("添加");
+            addBtn.setFont(new Font("微软雅黑", Font.PLAIN, 15));
+            addBtn.addActionListener(e -> {
+                String input = JOptionPane.showInputDialog(owner, "输入新的" + title + ":");
+                if (input == null) return;
+                String t = input.trim();
+                if (t.isEmpty()) return;
+                if (contains(t)) {
+                    JOptionPane.showMessageDialog(owner, "该前缀已存在:" + t,
+                            "提示", JOptionPane.WARNING_MESSAGE);
+                    return;
+                }
+                model.addElement(t);
+            });
+
+            JButton delBtn = new JButton("删除");
+            delBtn.setFont(new Font("微软雅黑", Font.PLAIN, 15));
+            delBtn.addActionListener(e -> {
+                int idx = list.getSelectedIndex();
+                if (idx < 0) return;
+                if (model.getElementAt(idx).endsWith(BUILTIN_TAG)) {
+                    JOptionPane.showMessageDialog(owner, "内置前缀由项目决定,不可删除",
+                            "提示", JOptionPane.WARNING_MESSAGE);
+                    return;
+                }
+                model.remove(idx);
+            });
+
+            JPanel btns = new JPanel();
+            btns.add(addBtn);
+            btns.add(delBtn);
+
+            JLabel head = new JLabel(title);
+            head.setFont(new Font("微软雅黑", Font.BOLD, 16));
+            panel.add(head, BorderLayout.NORTH);
+            panel.add(sp, BorderLayout.CENTER);
+            panel.add(btns, BorderLayout.SOUTH);
+        }
+
+        /** 是否已包含该前缀(内置项去掉标记后比较) */
+        private boolean contains(String value) {
+            if (value.equals(builtin)) return true;
+            for (int i = 0; i < model.size(); i++) {
+                String v = model.getElementAt(i);
+                if (v.endsWith(BUILTIN_TAG)) continue;
+                if (v.equals(value)) return true;
+            }
+            return false;
+        }
+
+        JPanel getPanel() { return panel; }
+
+        com.mes.core.ProjectConfigManager.PrefixKind getKind() { return kind; }
+
+        /** 用户追加部分(不含内置) */
+        List<String> collectExtras() {
+            List<String> out = new ArrayList<>();
+            for (int i = 0; i < model.size(); i++) {
+                String v = model.getElementAt(i);
+                if (v.endsWith(BUILTIN_TAG)) continue;
+                out.add(v);
+            }
+            return out;
+        }
+    }
+
+    /**
+     * 弹出前缀白名单配置对话框(工件码/冷板码/底护板码三类)
+     */
+    private void showPrefixWhitelistDialog() {
+        com.mes.core.ProjectConfigManager pc = com.mes.core.ProjectConfigManager.getInstance();
+        com.mes.core.ProjectConfigManager.Project cur = pc.getCurrentProject();
+
+        List<PrefixSection> sections = new ArrayList<>();
+        sections.add(new PrefixSection("工件码前缀",
+                com.mes.core.ProjectConfigManager.PrefixKind.PRODUCT, pc, this));
+        sections.add(new PrefixSection("冷板码前缀",
+                com.mes.core.ProjectConfigManager.PrefixKind.COLD_PLATE, pc, this));
+        sections.add(new PrefixSection("底护板码前缀",
+                com.mes.core.ProjectConfigManager.PrefixKind.BOTTOM_PLATE, pc, this));
+
+        JPanel grid = new JPanel(new GridLayout(1, 3, 10, 0));
+        for (PrefixSection s : sections) grid.add(s.getPanel());
+
+        JLabel head = new JLabel("当前项目:" + (cur != null ? cur.getDisplay() : "?"));
+        head.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        JLabel hint = new JLabel("<html><font color='gray' size='3'>"
+                + "内置前缀由当前项目决定,只回显不可删除;扫码时命中任一前缀即通过。<br>"
+                + "某一类全部为空时,回退到 station.yaml 里配置的前缀。</font></html>");
+
+        JPanel root = new JPanel(new BorderLayout(5, 8));
+        root.setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10));
+        root.add(head, BorderLayout.NORTH);
+        root.add(grid, BorderLayout.CENTER);
+        root.add(hint, BorderLayout.SOUTH);
+
+        int result = JOptionPane.showConfirmDialog(this, root,
+                "前缀白名单", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
+        if (result != JOptionPane.OK_OPTION) return;
+
+        for (PrefixSection s : sections) {
+            pc.setExtraPrefixes(s.getKind(), s.collectExtras());
+        }
+        refreshMenuTexts();
+
+        StringBuilder sb = new StringBuilder("前缀白名单已保存,下次扫码生效\n\n");
+        sb.append("工件码:").append(joinOrNone(pc.getEffectivePrefixes(
+                com.mes.core.ProjectConfigManager.PrefixKind.PRODUCT))).append('\n');
+        sb.append("冷板码:").append(joinOrNone(pc.getEffectivePrefixes(
+                com.mes.core.ProjectConfigManager.PrefixKind.COLD_PLATE))).append('\n');
+        sb.append("底护板码:").append(joinOrNone(pc.getEffectivePrefixes(
+                com.mes.core.ProjectConfigManager.PrefixKind.BOTTOM_PLATE)));
+        JOptionPane.showMessageDialog(this, sb.toString(), "保存成功", JOptionPane.INFORMATION_MESSAGE);
+    }
+
+    private static String joinOrNone(List<String> list) {
+        return (list == null || list.isEmpty()) ? "(不校验/走yaml)" : String.join(" / ", list);
+    }
+
     /**
      * 弹出修改拉铆设备IP对话框
      */