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<>();
@@ -95,6 +106,28 @@
                new String[]{"report_period", "region_name", "freight_m01", "freight_m02", "freight_m03", "freight_m04",
                        "freight_m05", "freight_m06", "freight_m07", "freight_m08", "freight_m09", "freight_m10", "freight_m11",
                        "freight_m12", "turnover_m01", "turnover_m12"});
        register("h2031_enterprise_monthly", "公路旅客月报H203-1表(道路旅客运输月度生产情况)",
                new String[]{"id", "report_period", "region_code", "enterprise_code", "enterprise_name", "vehicle_total",
                        "seat_total", "passenger_total", "turnover_total", "avg_distance_total", "verify_explanation"},
                new String[]{"主键", "报表期", "所属地区代码", "企业代码", "企业名称", "车辆数", "载客位数",
                        "客运量(万人)", "旅客周转量(万人公里)", "平均运距(公里)", "企业核实解释"},
                new String[]{"id", "report_period", "enterprise_name", "vehicle_total", "seat_total",
                        "passenger_total", "turnover_total", "avg_distance_total", "verify_explanation"});
        register("city_bus_monthly", "城市公交月度运营情况(企业级,含轨道/轮渡字段)",
                new String[]{"id", "report_period", "region_code", "city", "enterprise_name", "op_vehicles",
                        "passenger_volume", "turnover", "avg_distance", "passenger_chengxiang", "turnover_chengxiang",
                        "verify_explanation"},
                new String[]{"主键", "报表期", "所属地区代码", "市州", "企业名称", "运营车数", "客运量(万人次)",
                        "旅客周转量(万人公里)", "平均运距(公里)", "城际城乡客运量", "城际城乡周转量", "企业核实解释"},
                new String[]{"id", "report_period", "enterprise_name", "op_vehicles", "passenger_volume",
                        "turnover", "avg_distance", "verify_explanation"});
        register("city_taxi_monthly", "巡游出租汽车运营服务情况月报(市州级)",
                new String[]{"id", "report_period", "region_code", "city", "trip_total", "passenger_volume",
                        "turnover", "op_vehicles", "avg_distance", "verify_explanation"},
                new String[]{"主键", "报表期", "所属地区代码", "市州", "载客车次总数", "客运量(万人次)",
                        "旅客周转量(万人公里)", "运营车辆数", "平均运距(公里)", "企业核实解释"},
                new String[]{"id", "report_period", "city", "trip_total", "passenger_volume",
                        "turnover", "op_vehicles", "avg_distance", "verify_explanation"});
        register("audit_result", "审核结果",
                new String[]{"id", "rule_id", "report_id", "enterprise_code", "report_period", "actual_value",
                        "threshold_value", "deviation", "status", "review_comment", "created_at"},
@@ -124,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");
@@ -157,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());
@@ -174,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) {
@@ -217,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) {
@@ -259,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");