SpringBoot+PostgreSQL + 硅基流动大模型从零搭建 Text-to-SQL 智能问答系统
目录前言一、项目整体介绍1.1 什么是 Text-to-SQL1.2 系统核心功能1.3 技术选型说明二、本地环境准备2.1 开发环境硬性要求2.2 PostgreSQL 数据库初始化2.3 硅基流动 API Key 申请步骤三、项目完整目录结构四、Maven 依赖与全局配置4.1 pom.xml 完整依赖4.2 application.yml 配置详解五、数据库表结构与测试数据5.1 schema.sql 建表语句5.2 data.sql 初始化测试数据六、实体类与数据访问层代码6.1 Member.java 实体映射类6.2 MemberRepository 数据访问接口七、核心硅基流动大模型 API 对接服务 LlmService开发重点说明八、核心业务整合服务 Text2SqlService安全逻辑重点说明九、Controller 页面路由与 API 接口十、前端页面代码实现10.1 首页 index.html会员数据总览10.2 问答页面 chat.html核心交互页面十一、项目总结11.1 项目核心亮点11.2 开发踩坑 FAQQ1启动项目时报 PostgreSQL 连接失败Q2大模型返回 ERROR:API 调用失败Q3生成的 SQL 查询不出数据前言最近公司内部有个需求业务同事不会写 SQL每次查会员数据都要找后端开发帮忙来回沟通效率极低。想着能不能做一套自然语言转 SQL 的小工具业务人员输入中文描述就能自动查数据库不用懂任何数据库语法。调研了一圈方案本地部署大模型硬件成本太高、推理速度慢国内可直接调用的公有大模型 API 里硅基流动性价比不错对中文 SQL 生成适配很好搭配 SpringBootPostgreSQL 就能快速落地。本篇文章我会把完整搭建流程、踩坑细节、安全处理逻辑全部整理出来从环境准备、数据库建表、后端分层开发、大模型 API 对接再到前端问答页面一套完整流程直接照着跑就能出效果新手也能跟着实现。一、项目整体介绍1.1 什么是 Text-to-SQL简单说就是自然语言转结构化 SQL 语句非技术人员只用大白话描述查询需求后端对接大模型自动翻译成标准 SQL执行后把表格数据返回前端展示。 举个实际场景业务输入 “查询所有积分超过 1 万的金卡会员”系统自动生成SELECT * FROM member WHERE level 金卡会员 AND points 10000并展示匹配数据完全不用人工写查询语句。1.2 系统核心功能这套小系统主要做了 5 个实用能力满足基础业务查询场景自然语言智能问答中文提问自动生成 SQL 并返回数据表结果会员数据总览页面打开首页直接查看全量会员数据简单统计总条数SQL 可视化展示大模型生成的原始 SQL 清理后完整展示支持复制复用SQL 安全拦截机制强制只允许 SELECT 查询杜绝删表、改数据等危险操作防止注入风险自适应前端页面原生 JSThymeleaf 实现电脑、平板打开都能正常使用1.3 技术选型说明选型没有追求花里胡哨的框架选用成熟稳定、上手门槛低的技术方便后续二次改造分层技术栈版本选择理由后端主框架SpringBoot3.2.0生态完善Web、JPA、JDBC 一键集成企业主流技术栈数据库PostgreSQL14 及以上开源免费支持复杂查询、中文注释、自增序列企业数据分析常用ORM 层Spring Data JPA无指定版本简化单表 CRUD 开发不用手写基础 SQL原生 SQL 执行JdbcTemplate内置大模型生成动态 SQL 后需要原生执行工具返回表格数据页面模板Thymeleaf内置服务端渲染页面不用前后端分离小型工具开发更轻量化大模型服务硅基流动 API在线调用国内合规大模型服务中文理解强SQL 生成准确率高不用本地部署前端交互原生 JavaScript无框架无需引入 Vue/React减少打包、跨域等额外问题快速实现问答交互代码简化工具Lombok最新稳定版省去实体类 get/set/toString 冗余代码二、本地环境准备2.1 开发环境硬性要求JDK 版本最低 JDK17推荐 JDK21SpringBoot3.x 强制要求高版本 JDK低版本会直接启动报错构建工具Maven3.8 或者 Gradle8本文全程使用 Maven数据库PostgreSQL14 及以上本地安装或者 Docker 启动均可开发工具IDEA2023 以上版本最佳VS Code 搭配 Java 插件也能开发2.2 PostgreSQL 数据库初始化本地装好 PostgreSQL 后打开数据库客户端pgAdmin/psql 命令行执行下面 SQL新建专属数据库和用户避免和本地其他业务库冲突-- 创建项目专用数据库 CREATE DATABASE llm_texttosql; -- 创建数据库访问用户 CREATE USER postgres WITH PASSWORD postgres; -- 给用户分配该库全部操作权限 GRANT ALL PRIVILEGES ON DATABASE llm_texttosql TO postgres;踩坑提醒很多新手直接用 postgres 默认库存业务表后期多项目开发容易表名冲突单独建库是好习惯。2.3 硅基流动 API Key 申请步骤想要调用大模型生成 SQL必须先拿到接口密钥步骤很简单浏览器打开硅基流动官网完成手机号注册登录进入控制台 - API 密钥管理复制生成专属 Key注意妥善保存只展示一次模型选择新手推荐tencent/Hunyuan-MT-7B或Qwen/Qwen2.5-7B-Instruct对中文 SQL 适配度最高计费说明新用户一般赠送免费调用额度测试完全够用正式使用按需充值三、项目完整目录结构标准 SpringBoot 分层架构严格按照 Controller-Service-Repository 分层资源文件分类存放后续扩展多表、多接口不会混乱llm-text-to-sql/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/text2sql/ │ │ │ ├── Text2SqlApplication.java # 项目启动类 │ │ │ ├── config/ │ │ │ │ └── WebConfig.java # Web扩展配置本文基础版暂未拓展预留扩展 │ │ │ ├── controller/ │ │ │ │ └── Text2SqlController.java # 页面路由前后端API接口 │ │ │ ├── entity/ │ │ │ │ └── Member.java # 会员数据库实体映射类 │ │ │ ├── repository/ │ │ │ │ └── MemberRepository.java # JPA数据访问层 │ │ │ └── service/ │ │ │ ├── LlmService.java # 硅基流动大模型API对接核心类 │ │ │ └── Text2SqlService.java # 业务核心整合服务LLMSQL执行安全校验 │ │ └── resources/ │ │ ├── application.yml # 全局配置文件数据库、大模型参数全部写在这里 │ │ ├── schema.sql # 项目启动自动执行建表语句 │ │ ├── data.sql # 测试会员初始化数据 │ │ └── templates/ │ │ ├── index.html # 首页会员数据总览页面 │ │ └── chat.html # 智能问答交互页面 └── pom.xml # Maven依赖管理文件四、Maven 依赖与全局配置4.1 pom.xml 完整依赖所有用到的依赖全部贴出直接复制替换项目 pom.xml 即可无多余冗余包?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.0/version /parent groupIdcom.example/groupId artifactIdtext2sql/artifactId version1.0.0/version nametext2sql/name descriptionText-to-SQL会员智能问答系统/description properties java.version17/java.version /properties dependencies !-- SpringBoot Web容器提供接口访问能力 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf页面模板引擎渲染前端html -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- Spring Data JPA简化单表CRUD开发 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency !-- JdbcTemplate执行大模型生成的动态SQL -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency !-- PostgreSQL数据库驱动runtime运行时生效 -- dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId scoperuntime/scope /dependency !-- Lombok简化实体类代码不用手动写get/set -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration excludes exclude groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /exclude /excludes /configuration /plugin /plugins /build /project4.2 application.yml 配置详解所有配置做了详细注释新手能看懂每一项作用注意替换自己的硅基流动 API Key# 服务端口配置访问地址localhost:8080 server: port: 8080 spring: application: name: text2sql-llm-demo # PostgreSQL数据库连接配置和前面初始化库对应 datasource: url: jdbc:postgresql://localhost:5432/llm_texttosql username: postgres password: postgres driver-class-name: org.postgresql.Driver # JPA持久化配置 jpa: hibernate: ddl-auto: none # 关闭自动建表统一使用schema.sql脚本管理表结构线上更安全 show-sql: true # 控制台打印执行的SQL调试排错很方便 properties: hibernate: format_sql: true # 格式化打印SQL不会挤成一行 dialect: org.hibernate.dialect.PostgreSQLDialect # 项目启动自动执行初始化SQL脚本 sql: init: mode: always # 每次重启项目都执行脚本测试环境使用生产建议改成embedded schema-locations: classpath:schema.sql >五、数据库表结构与测试数据5.1 schema.sql 建表语句设计一张会员业务表覆盖姓名、等级、积分、入会时间等常用查询维度增加字段注释、索引提升查询效率-- 创建会员信息业务表 CREATE TABLE IF NOT EXISTS member ( id SERIAL PRIMARY KEY, -- 自增主键会员唯一ID name VARCHAR(50) NOT NULL, -- 会员姓名非空 gender VARCHAR(10), -- 性别男/女 age INTEGER, -- 年龄 phone VARCHAR(20) UNIQUE, -- 手机号唯一约束防止重复录入 email VARCHAR(100), -- 联系邮箱 level VARCHAR(20) DEFAULT 普通会员, -- 会员等级普通/银卡/金卡/钻石 points INTEGER DEFAULT 0, -- 账户积分 join_date DATE, -- 入会日期 status VARCHAR(20) DEFAULT 活跃, -- 账号状态活跃/冻结/注销 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- PostgreSQL专属字段中文注释数据库客户端可直接查看业务含义 COMMENT ON TABLE member IS 门店会员信息主表; COMMENT ON COLUMN member.id IS 会员主键ID; COMMENT ON COLUMN member.name IS 会员真实姓名; COMMENT ON COLUMN member.gender IS 会员性别; COMMENT ON COLUMN member.age IS 会员年龄; COMMENT ON COLUMN member.phone IS 绑定手机号; COMMENT ON COLUMN member.email IS 预留邮箱; COMMENT ON COLUMN member.level IS 会员会员等级; COMMENT ON COLUMN member.points IS 累计消费积分; COMMENT ON COLUMN member.join_date IS 首次入会时间; COMMENT ON COLUMN member.status IS 账号使用状态; COMMENT ON COLUMN member.created_at IS 数据创建时间; -- 建立常用查询字段索引大数据量下提升查询速度 CREATE INDEX IF NOT EXISTS idx_member_level ON member(level); CREATE INDEX IF NOT EXISTS idx_member_points ON member(points);5.2 data.sql 初始化测试数据批量插入多条测试会员数据加入ON CONFLICT冲突判断避免项目重复启动报唯一键冲突-- 初始化会员数据使用ON CONFLICT避免重复插入 INSERT INTO member (name, gender, age, phone, email, level, points, join_date, status) VALUES (张三, 男, 28, 13800138001, zhangsanexample.com, 金卡会员, 15800, 2023-01-15, 活跃), (李四, 女, 35, 13800138002, lisiexample.com, 钻石会员, 32000, 2022-03-20, 活跃), (王五, 男, 22, 13800138003, wangwuexample.com, 普通会员, 500, 2024-01-10, 活跃), (赵六, 女, 41, 13800138004, zhaoliuexample.com, 银卡会员, 8200, 2023-06-01, 活跃), (钱七, 男, 30, 13800138005, qianqiexample.com, 金卡会员, 12500, 2023-08-15, 活跃), (孙八, 女, 27, 13800138006, sunbaexample.com, 普通会员, 1200, 2024-02-20, 活跃), (周九, 男, 45, 13800138007, zhoujiuexample.com, 钻石会员, 45000, 2021-11-05, 活跃), (吴十, 女, 19, 13800138008, wushiexample.com, 普通会员, 200, 2024-05-01, 活跃), (郑十一, 男, 33, 13800138009, zheng11example.com, 银卡会员, 6800, 2023-04-10, 冻结), (冯十二, 女, 38, 13800138010, feng12example.com, 金卡会员, 18900, 2022-12-25, 活跃), (陈十三, 男, 25, 13800138011, chen13example.com, 普通会员, 800, 2024-03-18, 活跃), (褚十四, 女, 42, 13800138012, chu14example.com, 钻石会员, 52000, 2021-06-30, 活跃), (卫十五, 男, 29, 13800138013, wei15example.com, 金卡会员, 14200, 2023-09-12, 活跃), (蒋十六, 女, 36, 13800138014, jiang16example.com, 银卡会员, 9500, 2023-05-08, 活跃), (沈十七, 男, 23, 13800138015, shen17example.com, 普通会员, 350, 2024-04-22, 注销), (韩十八, 女, 48, 13800138016, han18example.com, 钻石会员, 61000, 2020-08-14, 活跃), (杨十九, 男, 31, 13800138017, yang19example.com, 金卡会员, 16500, 2023-07-19, 活跃), (朱二十, 女, 26, 13800138018, zhu20example.com, 普通会员, 950, 2024-01-05, 活跃), (秦廿一, 男, 39, 13800138019, qin21example.com, 银卡会员, 7800, 2023-02-28, 活跃), (尤廿二, 女, 34, 13800138020, you22example.com, 金卡会员, 13800, 2023-10-11, 活跃) ON CONFLICT (phone) DO NOTHING;六、实体类与数据访问层代码6.1 Member.java 实体映射类使用 Lombok 简化代码字段和数据库一一对应区分日期、时间类型package com.example.text2sql.entity; import jakarta.persistence.*; import lombok.Data; import java.time.LocalDate; import java.time.LocalDateTime; /** * 会员信息实体类 * * 对应数据库表member * * 字段说明 * - id: 会员ID主键自增 * - name: 会员姓名 * - gender: 性别男/女 * - age: 年龄 * - phone: 手机号唯一 * - email: 邮箱 * - level: 会员等级普通会员/银卡会员/金卡会员/钻石会员 * - points: 积分 * - joinDate: 入会日期 * - status: 状态活跃/冻结/注销 * - createdAt: 创建时间 */ Entity Table(name member) Data // Lombok注解自动生成getter/setter/toString等方法 public class Member { /** * 会员ID主键自增 */ Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; /** * 会员姓名非空最大长度50 */ Column(name name, nullable false, length 50) private String name; /** * 性别可选值男/女最大长度10 */ Column(name gender, length 10) private String gender; /** * 年龄 */ Column(name age) private Integer age; /** * 手机号唯一约束最大长度20 */ Column(name phone, length 20) private String phone; /** * 邮箱最大长度100 */ Column(name email, length 100) private String email; /** * 会员等级 * 可选值普通会员、银卡会员、金卡会员、钻石会员 * 默认值普通会员 */ Column(name level, length 20) private String level; /** * 积分默认值0 */ Column(name points) private Integer points; /** * 入会日期 */ Column(name join_date) private LocalDate joinDate; /** * 会员状态 * 可选值活跃、冻结、注销 * 默认值活跃 */ Column(name status, length 20) private String status; /** * 记录创建时间默认当前时间 */ Column(name created_at) private LocalDateTime createdAt; }6.2 MemberRepository 数据访问接口继承 JpaRepository 自带基础 CRUD额外扩展几个常用条件查询方法方便首页数据统计package com.example.text2sql.repository; import com.example.text2sql.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; /** * 会员数据访问接口 * * 功能说明 * - 基于Spring Data JPA提供Member实体的CRUD操作 * - 继承JpaRepository自动获得以下方法 * - findById(id): 根据ID查找会员 * - findAll(): 查找所有会员 * - save(member): 保存会员 * - deleteById(id): 根据ID删除会员 * - count(): 统计会员数量 * * 自定义查询方法Spring Data JPA自动生成SQL * - findByLevel(level): 根据会员等级查询 * - findByStatus(status): 根据状态查询 * - findByGender(gender): 根据性别查询 * - findByAgeGreaterThan(age): 查询年龄大于指定值的会员 * - findByPointsGreaterThan(points): 查询积分大于指定值的会员 */ Repository public interface MemberRepository extends JpaRepositoryMember, Long { /** * 根据会员等级查询会员列表 * * param level 会员等级普通会员/银卡会员/金卡会员/钻石会员 * return 该等级的会员列表 */ ListMember findByLevel(String level); /** * 根据会员状态查询会员列表 * * param status 会员状态活跃/冻结/注销 * return 该状态的会员列表 */ ListMember findByStatus(String status); /** * 根据性别查询会员列表 * * param gender 性别男/女 * return 该性别的会员列表 */ ListMember findByGender(String gender); /** * 查询年龄大于指定值的会员 * * param age 年龄阈值 * return 年龄大于指定值的会员列表 */ ListMember findByAgeGreaterThan(Integer age); /** * 查询积分大于指定值的会员 * * param points 积分阈值 * return 积分大于指定值的会员列表 */ ListMember findByPointsGreaterThan(Integer points); }七、核心硅基流动大模型 API 对接服务 LlmService这是整个项目最核心的类负责组装提示词、发起 HTTP 请求调用大模型、解析返回的 SQL 语句每一步都加了异常捕获接口调用失败会返回明确错误标识方便前端提示用户。package com.example.text2sql.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 大模型对接服务封装硅基流动API所有交互逻辑 */ Service public class LlmService { // 从yml配置文件读取大模型接口参数 Value(${siliconflow.api.url}) private String apiUrl; Value(${siliconflow.api.key}) private String apiKey; Value(${siliconflow.api.model}) private String model; private final ObjectMapper objectMapper; // 构造注入JSON序列化工具 public LlmService(ObjectMapper objectMapper) { this.objectMapper objectMapper; } /** * 接收用户自然语言调用大模型生成PostgreSQL标准SQL * param question 用户输入中文查询问题 * return 生成SQL异常统一返回ERROR:开头错误信息 */ public String textToSql(String question) { // 系统提示词告诉大模型数据库表结构、输出规范是SQL生成准确率关键 String systemPrompt 你是专业PostgreSQL SQL生成助手严格根据下方member表结构把用户中文问题转换成可直接执行的SQL语句。 数据库表member完整结构 - id: 会员ID (SERIAL PRIMARY KEY) - name: 会员姓名 (VARCHAR(50)) - gender: 性别 (VARCHAR(10)) 可选值仅男、女 - age: 年龄 (INTEGER) - phone: 手机号 (VARCHAR(20)) - email: 邮箱 (VARCHAR(100)) - level: 会员等级 (VARCHAR(20)) 可选值普通会员、银卡会员、金卡会员、钻石会员 - points: 累计积分 (INTEGER) - join_date: 入会日期 (DATE) - status: 账号状态 (VARCHAR(20)) 可选值活跃、冻结、注销 - created_at: 创建时间 (TIMESTAMP) 强制输出要求 1. 只输出纯SQL语句不要任何解释、说明文字 2. 严格遵循PostgreSQL语法中文字符串用单引号包裹 3. 用户问题无法生成有效查询时直接返回固定文本ERROR:无法理解的问题 4. 禁止生成DELETE、UPDATE、DROP、ALTER等修改、删除类SQL只输出SELECT相关语句 ; try { // 组装大模型请求体 MapString, Object requestBody new HashMap(); requestBody.put(model, model); // 系统角色消息表结构规则约束 MapString, String systemMsg Map.of(role, system, content, systemPrompt); // 用户提问消息 MapString, String userMsg Map.of(role, user, content, question); requestBody.put(messages, List.of(systemMsg, userMsg)); requestBody.put(max_tokens, 512); // temperature越低输出结果越固定、不会随意发挥SQL场景建议0.1 requestBody.put(temperature, 0.1); // JSON序列化请求参数 String jsonReq objectMapper.writeValueAsString(requestBody); // 创建HTTP客户端发起接口调用 HttpClient httpClient HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(30)) .build(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(apiUrl)) .header(Authorization, Bearer apiKey) .header(Content-Type, application/json) .timeout(Duration.ofSeconds(60)) .POST(HttpRequest.BodyPublishers.ofString(jsonReq)) .build(); // 接收接口返回结果 HttpResponseString response httpClient.send(request, HttpResponse.BodyHandlers.ofString()); // 接口正常返回200状态码解析生成的SQL if (response.statusCode() 200) { JsonNode root objectMapper.readTree(response.body()); JsonNode choices root.get(choices); if (choices ! null choices.isArray() choices.size() 0) { return choices.get(0).get(message).get(content).asText().trim(); } return ERROR:大模型返回数据格式异常; } else { // 接口调用失败返回状态码方便排查 return ERROR:API调用失败HTTP状态码: response.statusCode(); } } catch (Exception e) { // 捕获所有网络、序列化异常统一封装错误信息 return ERROR: e.getMessage(); } } }开发重点说明提示词设计完整把表字段、枚举值、语法规则全部传给大模型是避免生成错误 SQL 的核心如果后期 SQL 经常出错优先优化提示词而非调整代码。temperature 参数文本创作场景会调高到 0.7-1但是 SQL 生成需要精准设置 0.1 限制模型随机发挥。统一错误前缀所有异常都用ERROR:开头上层业务服务直接判断前缀就能区分正常 SQL 和错误信息逻辑更简洁。八、核心业务整合服务 Text2SqlService串联大模型调用、SQL 清洗、安全校验、数据库执行整套流程增加安全拦截逻辑防止用户诱导大模型生成删改数据 SQL是保障系统安全的关键层。package com.example.text2sql.service; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import java.util.*; /** * Text-to-SQL整合业务服务串联大模型、SQL安全校验、数据库执行 */ Service public class Text2SqlService { private final LlmService llmService; private final JdbcTemplate jdbcTemplate; private final ObjectMapper objectMapper; // 构造注入依赖 public Text2SqlService(LlmService llmService, JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) { this.llmService llmService; this.jdbcTemplate jdbcTemplate; this.objectMapper objectMapper; } /** * 完整问答处理主流程 * 1.调用大模型生成SQL → 2.清理SQL多余标记 → 3.安全校验只允许SELECT → 4.执行查询返回数据 * param question 用户输入中文问题 * return 包含原始问题、生成SQL、查询结果、成功/失败标识的Map */ public MapString, Object query(String question) { MapString, Object resultMap new HashMap(); resultMap.put(question, question); // 第一步调用大模型获取SQL String rawSql llmService.textToSql(question); resultMap.put(generatedSql, rawSql); // 判断大模型是否返回错误 if (rawSql.startsWith(ERROR)) { resultMap.put(success, false); resultMap.put(error, rawSql); return resultMap; } // 第二步清洗SQL去除AI附带的markdown代码块标记 String cleanSql cleanSqlText(rawSql); resultMap.put(cleanedSql, cleanSql); try { // 第三步安全校验拦截非查询类SQL if (!checkSqlSafe(cleanSql)) { resultMap.put(success, false); resultMap.put(error, 安全拦截系统仅支持SELECT查询语句禁止修改/删除数据操作); return resultMap; } // 第四步执行动态SQL返回表格数据 ListMapString, Object dataList jdbcTemplate.queryForList(cleanSql); resultMap.put(success, true); resultMap.put(data, dataList); resultMap.put(count, dataList.size()); } catch (Exception e) { // SQL语法错误、表不存在等数据库异常捕获 resultMap.put(success, false); resultMap.put(error, SQL执行失败 e.getMessage()); } return resultMap; } /** * 清理大模型返回SQL附带的多余符号比如sql、、SQL:前缀 */ private String cleanSqlText(String sql) { if (sql null || sql.isEmpty()) return ; sql sql.replaceAll(sql, ).replaceAll(, ).trim(); if (sql.toLowerCase().startsWith(sql:)) { sql sql.substring(4).trim(); } return sql; } /** * SQL安全校验仅允许SELECT开头支持WITH子句CTE查询 * 拦截UPDATE/DELETE/DROP/ALTER等危险操作规避数据安全风险 */ private boolean checkSqlSafe(String sql) { String upperSql sql.toUpperCase().trim(); return upperSql.startsWith(SELECT) || upperSql.startsWith(WITH); } /** * 查询全部会员数据首页总览页面使用 */ public ListMapString, Object getAllMemberData() { return jdbcTemplate.queryForList(SELECT * FROM member ORDER BY id ASC); } }安全逻辑重点说明很多新手做 Text-to-SQL 项目会忽略安全问题直接执行大模型返回的 SQL一旦有人刻意诱导大模型生成DROP TABLE member整张业务表会直接删除。本文做了两层防护提示词约束大模型禁止生成修改类 SQL代码层二次校验 SQL 开头关键字双重拦截避免数据事故。九、Controller 页面路由与 API 接口区分页面跳转接口和 JSON 数据接口使用 Controller 返回页面ResponseBody 返回 JSON接口注释清晰方便后续对接前端或者第三方系统。package com.example.text2sql.controller; import com.example.text2sql.service.Text2SqlService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; /** * 系统控制器页面跳转、前端问答API统一处理 */ Controller public class Text2SqlController { private final Text2SqlService text2SqlService; public Text2SqlController(Text2SqlService text2SqlService) { this.text2SqlService text2SqlService; } /** * 首页会员数据总览页面 */ GetMapping(/) public String indexPage(Model model) { ListMapString, Object allMember text2SqlService.getAllMemberData(); model.addAttribute(members, allMember); model.addAttribute(totalCount, allMember.size()); return index; } /** * 智能问答聊天页面 */ GetMapping(/chat) public String chatPage() { return chat; } /** * 问答核心API接口前端AJAX异步调用 * 请求体{question:查询钻石会员} */ PostMapping(/api/query) ResponseBody public MapString, Object queryData(RequestBody MapString, String request) { String question request.get(question); if (question null || question.trim().length() 0) { return Map.of(success, false, error, 输入内容不能为空请描述你的查询需求); } return text2SqlService.query(question); } /** * 获取全量会员数据接口可供第三方调用 */ GetMapping(/api/members) ResponseBody public ListMapString, Object getMemberList() { return text2SqlService.getAllMemberData(); } }十、前端页面代码实现10.1 首页 index.html会员数据总览使用 Thymeleaf 循环渲染会员表格简单统计会员总数页面样式简洁适配办公场景。body div classheader h1 全部会员数据总览/h1 div classnav a href/chat进入AI智能问答/a /div /div div classcount-card h3当前会员总条数span th:text${totalCount}0/span/h3 /div table thead tr thID/th th姓名/th th性别/th th年龄/th th手机号/th th会员等级/th th累计积分/th th账号状态/th th入会日期/th /tr /thead tbody tr th:eachitem : ${members} td th:text${item.id}/td td th:text${item.name}/td td th:text${item.gender}/td td th:text${item.age}/td td th:text${item.phone}/td td th:text${item.level}/td td th:text${item.points}/td td th:text${item.status}/td td th:text${item.join_date}/td /tr /tbody /table /body展示效果如下10.2 问答页面 chat.html核心交互页面原生 JS 实现表单提交、异步请求、结果渲染自带示例快捷提问按钮增加 HTML 转义函数防止 XSS 攻击展示生成 SQL 和查询表格。script // 填充示例问题到输入框 function fillExample(text) { document.getElementById(questionInput).value text; } // 表单提交监听 const form document.getElementById(queryForm); form.addEventListener(submit, async function(e) { e.preventDefault(); const inputVal document.getElementById(questionInput).value.trim(); if (!inputVal) { alert(请输入查询问题); return; } try { // 调用后端查询接口 const res await fetch(/api/query, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({question: inputVal}) }); const result await res.json(); renderResult(inputVal, result); } catch (err) { alert(网络请求失败 err.message); } }); // 渲染查询结果到页面 function renderResult(question, res) { let htmlStr div classresult-item div classquestion❓ 你的问题${htmlEscape(question)}/div; if (res.success) { htmlStr div生成SQL语句/div div classsql-block${htmlEscape(res.cleanedSql)}/div div匹配到${res.count}条数据/div table thead tr ${Object.keys(res.data[0] || {}).map(k th${htmlEscape(k)}/th).join()} /tr /thead tbody ${res.data.map(row tr ${Object.values(row).map(v td${htmlEscape(v || )}/td).join()} /tr ).join()} /tbody /table ; } else { htmlStr div classerror-text❌ 查询失败${htmlEscape(res.error)}/div; } htmlStr /div; // 最新结果插入最上方 document.getElementById(resultContainer).insertAdjacentHTML(afterbegin, htmlStr); } // HTML转义防止XSS注入 function htmlEscape(text) { const div document.createElement(div); div.textContent text; return div.innerHTML; } /script十一、项目总结11.1 项目核心亮点完整可落地从数据库建表、后端分层、大模型对接、前端页面全套代码复制即可运行无缺失模块双层 SQL 安全防护提示词约束 代码关键字拦截杜绝删改表等高危操作企业内部使用更放心轻量化无复杂依赖不引入 Vue、Redis、消息队列等重型组件小型工具快速开发部署用户友好前端自带示例快捷提问自动展示生成 SQL业务人员可以复制 SQL 复用完善异常捕获大模型接口报错、SQL 语法错误、空输入全部做友好提示便于排查问题。11.2 开发踩坑 FAQQ1启动项目时报 PostgreSQL 连接失败A检查 yml 数据库 url、账号密码确认本地 PostgreSQL 服务正常启动5432 端口没有被占用同时确认 llm_texttosql 数据库已提前创建。Q2大模型返回 ERROR:API 调用失败A核对硅基流动 API Key 是否复制正确Key 前后不要带空格检查本地网络是否能访问硅基流动外网接口新用户查看是否还有免费调用额度。Q3生成的 SQL 查询不出数据A优先检查提示词里的字段枚举值是否和数据库一致比如 “金卡会员” 不能写成 “金卡”其次优化提示词增加 1-2 条查询示例给大模型参考提升匹配准确率。这套 Text-to-SQL 系统完全适配中小企业内部数据查询场景不用业务人员学习 SQL 语法开发维护成本很低。文章里所有代码都是实际运行调试后的完整版本大家可以直接复制搭建有任何搭建报错、功能拓展的问题。行文仓促定有不足之处欢迎各位朋友在评论区批评指正不胜感激。