zhizhijie
2026-09-14 d1cfd849c6a61800dcd86ffb45f80882ae20b2e7
traffic-audit-server/src/main/java/com/trafficaudit/llmintegration/service/DbQueryExecutor.java
@@ -28,6 +28,17 @@
    @Resource
    private JdbcTemplate jdbcTemplate;
    @Resource
    private DesensitizeService desensitizeService;
    /** 禁止外发到大模型的字段:个人与主体身份类信息(在可查列、过滤列、排序列中一并屏蔽) */
    private static final java.util.Set<String> BLOCKED_COLUMNS = new java.util.TreeSet<>(Arrays.asList(
            "unified_credit_code", "contact_person", "contact_phone",
            "unit_leader", "stats_leader", "report_unit", "enterprise_code"));
    /** 自由文本里残留的手机号 */
    private static final Pattern PHONE = Pattern.compile("(?<!\\d)1[3-9]\\d{9}(?!\\d)");
    public static final Set<String> ALLOWED_TABLES = new TreeSet<>();
    private static final Map<String, TableMeta> TABLES = new LinkedHashMap<>();
@@ -146,7 +157,7 @@
        for (TableMeta meta : TABLES.values()) {
            sb.append(meta.table).append("【").append(meta.desc).append("】: ");
            List<String> parts = new ArrayList<>();
            for (String c : meta.cols) {
            for (String c : selectable(meta)) {
                parts.add(snakeToCamel(c) + "(" + meta.labels.getOrDefault(c, snakeToCamel(c)) + ")");
            }
            sb.append(String.join(", ", parts)).append("\n");
@@ -179,6 +190,7 @@
            sql.append(" LIMIT ").append(lim);
            List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), w.args.toArray());
            maskRows(rows);
            out.set("success", true);
            out.set("rows", rows);
            out.set("count", rows.size());
@@ -196,21 +208,68 @@
        if (columnsParam == null) {
            return aliased(meta.defaults);
        }
        // 传入 "*" 时只返回默认列,不再放开整表(敏感字段不参与查询)
        if (columnsParam instanceof String && "*".equals(columnsParam)) {
            return aliased(meta.cols);
            return aliased(meta.defaults);
        }
        if (columnsParam instanceof List) {
            List<String> out = new ArrayList<>();
            List<String> usable = selectable(meta);
            for (Object o : (List<?>) columnsParam) {
                String c = normalizeCol(String.valueOf(o));
                if (!meta.cols.contains(c)) {
                    throw new IllegalArgumentException("列名不合法: " + o + ",该表可用列: " + camelList(meta.cols));
                if (BLOCKED_COLUMNS.contains(c)) {
                    throw new IllegalArgumentException("该字段不允许查询: " + o);
                }
                if (!usable.contains(c)) {
                    throw new IllegalArgumentException("列名不合法: " + o + ",该表可用列: " + camelList(usable));
                }
                out.add(c);
            }
            return aliased(out.isEmpty() ? meta.defaults : out);
        }
        return aliased(meta.defaults);
    }
    /** 外发前脱敏:企业名称替换为别名,自由文本中的企业名称与手机号一并处理 */
    private void maskRows(List<Map<String, Object>> rows) {
        if (rows == null) {
            return;
        }
        for (Map<String, Object> row : rows) {
            Object nameObj = row.get("enterpriseName");
            String realName = nameObj == null ? null : String.valueOf(nameObj);
            String alias = null;
            if (realName != null && !realName.isEmpty()) {
                alias = desensitizeService.maskEnterprise(realName);
                row.put("enterpriseName", alias);
            }
            for (Map.Entry<String, Object> e : row.entrySet()) {
                Object v = e.getValue();
                if (!(v instanceof String)) {
                    continue;
                }
                String s = (String) v;
                String outText = s;
                if (realName != null && !realName.isEmpty() && outText.contains(realName)) {
                    outText = outText.replace(realName, alias);
                }
                outText = PHONE.matcher(outText).replaceAll("手机号已脱敏");
                if (!outText.equals(s)) {
                    e.setValue(outText);
                }
            }
        }
    }
    /** 该表可查字段 = 全部字段 - 敏感字段 */
    private static List<String> selectable(TableMeta meta) {
        List<String> out = new ArrayList<>();
        for (String c : meta.cols) {
            if (!BLOCKED_COLUMNS.contains(c)) {
                out.add(c);
            }
        }
        return out;
    }
    private List<String> aliased(List<String> cols) {
@@ -239,8 +298,9 @@
        }
        for (Map.Entry<String, Object> e : filters.entrySet()) {
            String col = normalizeCol(String.valueOf(e.getKey()));
            if (!meta.cols.contains(col)) {
                throw new IllegalArgumentException("列名不合法: " + e.getKey() + ",该表可用列: " + camelList(meta.cols));
            List<String> usable = selectable(meta);
            if (BLOCKED_COLUMNS.contains(col) || !usable.contains(col)) {
                throw new IllegalArgumentException("列名不合法: " + e.getKey() + ",该表可用列: " + camelList(usable));
            }
            Object v = e.getValue();
            if (v instanceof List) {
@@ -281,7 +341,7 @@
            s = s.substring(0, s.length() - 4).trim();
        }
        String col = normalizeCol(s);
        if (!meta.cols.contains(col)) {
        if (BLOCKED_COLUMNS.contains(col) || !selectable(meta).contains(col)) {
            throw new IllegalArgumentException("排序列不合法: " + orderBy);
        }
        return col + (desc ? " DESC" : " ASC");