☰
JSP+MySQL法律援助系统实战:从建表到部署的完整案例
2026/10/9 16:21:40 网站建设 项目流程

简介:本资源是一套基于Java Web技术栈的法律援助与咨询系统完整项目源码,面向计算机专业学生、Java初学者及需要课程设计或毕业设计参考的开发者。系统采用JSP作为前台展示语言,后台以轻量级MySQL管理数据,开发环境为MyEclipse8.5,运行于Tomcat服务器,实现了法律咨询、援助申请、用户后台登录与管理等核心功能,管理员与教师均可通过后台进行统一管理。项目涉及JSP、Spring、SSM、Maven等关键技术,数据库建议使用MySQL5.7以上版本,可配合Navicat或SQLyog进行管理。压缩包为zip格式,大小约5.72MB,内含项目源码与说明文档,便于快速导入IDEA或Eclipse运行调试。目前已有106人学习浏览,适合作为SSM框架入门练手、课程设计参考或二次开发基础,帮助读者理解前后台分离管理、数据库设计与JSP页面交互的完整实现思路。

1. 法律援助与咨询系统:为什么 JSP + MySQL 这套老组合还在被反复翻出来用

打开招聘网站搜“java工程师”,你会发现一个有意思的现象:大量中小型律所、法律援助中心、高校法学院的内部管理系统,技术栈还停留在 JSP + Servlet + MySQL。不是他们不想升级,而是这类系统有个共同特征——业务逻辑不复杂,但表单流转密集、角色权限清晰、部署环境往往是一台内网 Windows Server。用 Spring Boot 重写一遍,收益不明显,成本却不低。

法律援助与咨询系统的核心诉求其实很朴素:当事人提交申请、工作人员审核分流、律师接单回复、管理员统计归档。JSP 天然适合做这种“表单进、列表出”的页面,MySQL 存结构化案件数据绰绰有余。这套源码的价值不在于技术多新,而在于它把申请单状态机、角色菜单隔离、咨询记录关联这三件事用最直白的方式写清楚了。如果你正在做 javaweb 项目完整案例、需要一套能跑通的 mysql项目实战代码,或者想理解“不依赖框架的 MVC 到底怎么落地”,这个方向值得花两小时跟一遍。

2. 从建表到跑通:法律援助系统的数据库设计与 JSP 页面骨架

2.1 先想清楚三张核心表和它们的状态流转

法律援助系统的数据模型不需要花哨,但状态字段的设计直接决定后面代码好不好写。我一般会先画三个实体:用户(当事人/律师/管理员共用一张表加角色字段)、申请单、咨询记录。

申请单的状态流转是业务核心:待审核 → 已通过 → 已指派 → 已办结,中间可能插入已驳回。很多新手会把状态写成中文直接存 varchar,后面做条件查询时踩坑。正确做法是用 tinyint 映射,在 Java 侧用常量或枚举解释。

-- 用户表:用 role 区分身份,避免三张表联查 CREATE TABLE `sys_user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(64) NOT NULL COMMENT 'MD5存储,别存明文', `real_name` varchar(30) DEFAULT NULL, `role` tinyint NOT NULL DEFAULT '0' COMMENT '0当事人 1律师 2管理员', `phone` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 申请单表:status 用数字,create_time 建索引 CREATE TABLE `legal_apply` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL COMMENT '提交人', `lawyer_id` int DEFAULT NULL COMMENT '指派律师', `case_type` varchar(20) DEFAULT NULL COMMENT '案件类型', `content` text COMMENT '案情描述', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待审 1通过 2驳回 3已指派 4办结', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表时有两个参数值得注意:utf8mb4而不是utf8,因为案情描述里可能出现生僻字或特殊符号;create_time用CURRENT_TIMESTAMP默认值,省去 Java 侧手动赋值,减少时区处理出错概率。idx_status索引在后台按状态筛选列表时能明显提速,数据量过万后差别肉眼可见。

2.2 JSP 页面骨架:把公共部分抽出来,别每个页面复制导航栏

JSP 最容易被写烂的地方就是每个页面都复制一遍 header 和 sidebar。正确做法是用<%@ include file="" %>静态包含或<jsp:include>动态包含。静态包含在编译期合并,适合导航栏这种不常变的;动态包含在运行期请求,适合需要传参的。

<!-- /common/header.jsp 抽出来的公共头 --> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% // 从 session 取当前用户,没登录直接踢回登录页 Object loginUser = session.getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } %> <div class="topbar"> <span>法律援助与咨询系统</span> <span>当前用户:${sessionScope.loginUser.realName}</span> </div>

然后在业务页面里这样引入:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ include file="/common/header.jsp" %> <h2>我的申请记录</h2> <!-- 下面接列表渲染 -->

这里有个血泪经验:include的路径用/开头是相对于 Web 应用根目录,不要写成相对当前文件的路径,否则页面一挪目录就 404。另外session.getAttribute返回 Object,强转前一定判空,否则用户直接访问内页时抛 NullPointerException,页面白屏,排查半天。

2.3 用 Servlet 做控制器,把 JDBC 查询封装成工具类

不依赖框架时,Servlet 就是 Controller。每个业务动作对应一个 Servlet 或一个 Servlet 里的不同分支。数据库连接不要每次DriverManager.getConnection,写一个工具类从db.properties读配置。

// DBUtil.java 关键部分 public class DBUtil { private static String url; private static String user; private static String pwd; static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties p = new Properties(); p.load(in); url = p.getProperty("jdbc.url"); user = p.getProperty("jdbc.user"); pwd = p.getProperty("jdbc.password"); Class.forName(p.getProperty("jdbc.driver")); } catch (Exception e) { throw new RuntimeException("数据库配置加载失败", e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(url, user, pwd); } }

db.properties放在src根目录下,编译后自动进WEB-INF/classes。参数含义:jdbc.url里要带useUnicode=true&characterEncoding=utf8,否则中文案情描述存进去变问号;jdbc.driver用com.mysql.cj.jdbc.Driver,MySQL 8.0 之后旧驱动类名已废弃。查询时用PreparedStatement占位符,别拼字符串,这是防 SQL 注入的底线。

3. 申请提交与审核流转:JSP 表单到 MySQL 的完整链路怎么写

3.1 表单提交的编码问题与文件上传处理

法律援助申请往往要上传身份证、低保证明等附件。纯 JSP + Servlet 处理文件上传需要commons-fileupload和commons-io两个 jar。表单必须设enctype="multipart/form-data",此时request.getParameter直接取不到普通字段,得用ServletFileUpload解析。

// ApplySubmitServlet 核心逻辑 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // 判断是否为 multipart 请求 boolean isMultipart = ServletFileUpload.isMultipartContent(req); if (!isMultipart) { resp.sendRedirect("apply.jsp?err=noFile"); return; } DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); // 1MB 内存缓冲 ServletFileUpload upload = new ServletFileUpload(factory); upload.setHeaderEncoding("UTF-8"); upload.setFileSizeMax(5 * 1024 * 1024); // 单文件 5MB 上限 try { List<FileItem> items = upload.parseRequest(req); Map<String, String> fields = new HashMap<>(); for (FileItem item : items) { if (item.isFormField()) { fields.put(item.getFieldName(), item.getString("UTF-8")); } else { // 文件保存逻辑,重命名防覆盖 String fileName = System.currentTimeMillis() + "_" + item.getName(); item.write(new File(uploadDir, fileName)); } } // 拿到 fields 后走 JDBC 插入 } catch (FileUploadException e) { // 文件超限会走到这里,给用户明确提示 req.setAttribute("msg", "文件超过5MB,请压缩后重试"); req.getRequestDispatcher("apply.jsp").forward(req, resp); } }

参数说明:setSizeThreshold控制多大文件写磁盘,小文件留内存快;setFileSizeMax是单文件硬上限,超了抛异常,必须捕获后给用户友好提示,否则用户看到 500 页面直接流失。item.getString("UTF-8")里的编码参数不能省,否则中文案件类型乱码。

3.2 审核状态更新:一条 SQL 背后的并发问题

管理员点“通过”时,典型写法是UPDATE legal_apply SET status=1 WHERE id=?。单人在用没问题,但两个管理员同时打开同一条待审记录、先后点通过和驳回,后执行的会覆盖前者。解决办法是在 WHERE 里带上原状态做乐观锁。

UPDATE legal_apply SET status = ?, lawyer_id = ? WHERE id = ? AND status = 0;

Java 侧判断executeUpdate()返回的影响行数:返回 1 说明抢到了,返回 0 说明这条记录已被别人处理过,给用户提示“该申请已被处理,请刷新列表”。这个模式在工单系统、订单系统里通用,属于花五分钟加上、省两小时扯皮的改动。

3.3 列表分页:别用 LIMIT 全表扫,也别在 JSP 里写 Java 循环

申请列表数据量大了必须分页。MySQL 用LIMIT offset, size,但 offset 很大时性能下降明显。法律援助系统单表通常到不了百万级,LIMIT够用,但要注意两点:一是先查COUNT(*)算总页数,二是排序字段必须有索引。

// 分页查询封装 public List<LegalApply> pageQuery(int pageNo, int pageSize, int status) { List<LegalApply> list = new ArrayList<>(); String sql = "SELECT * FROM legal_apply WHERE status = ? " + "ORDER BY create_time DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, status); ps.setInt(2, (pageNo - 1) * pageSize); ps.setInt(3, pageSize); ResultSet rs = ps.executeQuery(); while (rs.next()) { LegalApply a = new LegalApply(); a.setId(rs.getInt("id")); a.setCaseType(rs.getString("case_type")); a.setStatus(rs.getInt("status")); a.setCreateTime(rs.getTimestamp("create_time")); list.add(a); } } catch (SQLException e) { e.printStackTrace(); } return list; }

JSP 侧只负责渲染,用 JSTL 的<c:forEach>遍历,不要在页面里写<% for(...) %>脚本段。脚本段一多,页面和逻辑搅在一起,改一个字段要翻三个文件。ORDER BY create_time DESC配合idx_status索引,先过滤再排序,执行计划走 range 扫描,比全表 filesort 快一个量级。

4. 角色权限与咨询回复:JSP 里怎么做菜单隔离和会话控制

4.1 用 Filter 统一拦截,别在每个 JSP 顶部写 if

前面 header.jsp 里写了 session 判空,但那只防了未登录。当事人直接访问/admin/auditList.jsp这种管理员页面,header 拦不住。正确做法是加一个权限 Filter,按 URL 前缀和角色做映射。

// AuthFilter.java public class AuthFilter implements Filter { // 路径前缀 -> 允许的角色 private static final Map<String, Integer> RULE = new HashMap<>(); static { RULE.put("/admin/", 2); // 管理员 RULE.put("/lawyer/", 1); // 律师 RULE.put("/user/", 0); // 当事人 } public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String path = request.getRequestURI() .substring(request.getContextPath().length()); Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } SysUser u = (SysUser) user; for (Map.Entry<String, Integer> e : RULE.entrySet()) { if (path.startsWith(e.getKey()) && u.getRole() != e.getValue()) { response.sendError(403, "无权访问"); return; } } chain.doFilter(req, resp); } }

web.xml里配置<filter-mapping>拦截/*。参数含义:getRequestURI()包含 contextPath,减掉后才是应用内路径,这一步漏了会导致匹配永远失败。角色用数字比较,别用字符串 equals,少一层空指针风险。

4.2 咨询回复的关联查询:一次 JOIN 拿到申请人和律师信息

咨询记录表存apply_id、lawyer_id、reply_content。列表展示时要显示“张三的工伤申请,李律师回复:……”。新手容易先查咨询表再循环查用户表,N+1 问题。一条 JOIN 解决。

SELECT c.id, c.reply_content, c.reply_time, u1.real_name AS applicant, u2.real_name AS lawyer, a.case_type FROM consult_record c JOIN legal_apply a ON c.apply_id = a.id JOIN sys_user u1 ON a.user_id = u1.id JOIN sys_user u2 ON c.lawyer_id = u2.id WHERE c.apply_id = ? ORDER BY c.reply_time ASC

四个表关联看着多,但都是主键或索引关联,实际执行很快。注意sys_user被用了两次,必须起别名u1、u2,否则列名歧义报错。回复时间正序排列,让对话按时间线展示,符合阅读习惯。

4.3 会话超时与重复登录的处理

session.setMaxInactiveInterval(30 * 60)设半小时超时。用户超时后点提交,session 里取不到用户,如果代码没判空就抛异常。更隐蔽的问题是同一账号在两台机器登录,后登录的会覆盖前一个的 session 数据,前一个用户操作时身份错乱。简单做法是在用户表加last_login_token,每次登录生成 UUID 存 session 和数据库,Filter 里比对,不一致就强制下线。这个改动不大,但能避免“我明明没操作,记录却变了”这类玄学问题。

5. 部署与联调避坑:JSP + MySQL 项目最容易翻车的五个地方

5.1 现象:页面中文全是问号,数据库里也是问号

原因:三层编码没统一。JSP 页面pageEncoding、响应contentType、JDBC URL 的characterEncoding、MySQL 建库字符集,任何一层是ISO-8859-1或utf8(非 mb4)都会出问题。

解决:JSP 顶部写<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;JDBC URL 加useUnicode=true&characterEncoding=utf8;建库用CREATE DATABASE legal_aid DEFAULT CHARSET utf8mb4。四处对齐后重启 Tomcat,清浏览器缓存再试。

5.2 现象:ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因:MySQL 驱动 jar 没放进WEB-INF/lib,或者放进了项目根目录但没加入构建路径。Tomcat 只认WEB-INF/lib下的 jar。

解决:把mysql-connector-java的 jar 复制到WEB-INF/lib,在 IDE 里刷新项目。如果用的是 Maven,确认<scope>不是provided,provided 的包不会打进 war。

5.3 现象:修改 JSP 后刷新页面没变化

原因:Tomcat 默认对 JSP 有编译缓存,或者浏览器缓存了旧页面。开发阶段频繁遇到。

解决:Tomcat 的conf/web.xml里把development设为true,modificationTestInterval设为0,让每次请求都检查 JSP 是否改动。浏览器侧用 Ctrl+F5 强制刷新,或者开 DevTools 勾选 Disable cache。

5.4 现象:上传大文件时页面卡死或 500

原因:commons-fileupload默认把整个请求读进内存,文件大了直接 OOM;或者setFileSizeMax抛的异常没捕获,冒到容器层。

解决:DiskFileItemFactory.setSizeThreshold设 1MB 左右,超过写临时文件;setFileSizeMax按业务定,法援材料一般 5MB 够;parseRequest外面必须包 try-catch,捕获FileUploadBase.SizeLimitExceededException单独提示。

5.5 现象:部署到服务器后图片和附件显示 404

原因:上传目录写的是开发机绝对路径,比如D:/upload,服务器上没这个盘。或者上传目录在 webapp 下,重新部署 war 包时被覆盖清空。

解决:上传路径配在db.properties或独立配置文件里,用相对路径基于System.getProperty("catalina.home")拼接;存储目录放在 webapps 之外,Tomcat 里配<Context docBase>映射虚拟路径。这样重新部署不会丢文件。

6. 把系统跑稳之后:三个能立刻用上的加固技巧

第一个技巧是给所有列表查询加默认时间范围。法律援助系统的申请单会越积越多,管理员打开列表默认查全部,几个月后页面加载明显变慢。在 Servlet 里判断:如果请求没带startDate参数,默认取最近 30 天。这个改动一行代码,但避免了“系统越用越卡”的投诉。用户真要查历史,自己选日期范围,查询条件带上create_time BETWEEN ? AND ?,走索引。

第二个技巧是登录密码别用 MD5 裸存。MD5 已经被彩虹表打穿,加盐也挡不住 GPU 暴力破解。换成BCrypt,jbcrypt一个 jar 包搞定,BCrypt.hashpw(pwd, BCrypt.gensalt())存库,校验用BCrypt.checkpw。老系统迁移时,可以在用户表加pwd_version字段,登录时如果是旧 MD5 就验证通过后自动升级为 BCrypt,用户无感知。

第三个技巧是给关键操作留操作日志。审核通过、驳回、指派律师这三个动作,往oper_log表插一条记录:谁、什么时候、对哪条申请、做了什么。表结构简单:id, user_id, apply_id, action, create_time。出纠纷时这是唯一的后悔药。写日志用异步或至少放在业务事务提交之后,别因为日志插入失败把主业务回滚了。

// 操作日志记录,放在业务成功后调用 public void log(int userId, int applyId, String action) { String sql = "INSERT INTO oper_log(user_id, apply_id, action, create_time) " + "VALUES(?, ?, ?, NOW())"; try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setInt(2, applyId); ps.setString(3, action); ps.executeUpdate(); } catch (SQLException e) { // 日志失败不影响主流程,只打印 e.printStackTrace(); } }

这三个技巧都不改变原有架构,加起来不到半天工作量,但系统从“能跑”变成“敢长期用”。我自己维护过一套类似系统,上线头三个月没加操作日志,后来两次申请状态对不上,查了一下午数据库 binlog 才定位到是并发覆盖。那之后我养成的习惯是:凡是状态会变的表,要么加乐观锁,要么加日志,最好两个都加。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询