简介本资源是一套基于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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 申请单表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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时有两个参数值得注意utf8mb4而不是utf8因为案情描述里可能出现生僻字或特殊符号create_time用CURRENT_TIMESTAMP默认值省去 Java 侧手动赋值减少时区处理出错概率。idx_status索引在后台按状态筛选列表时能明显提速数据量过万后差别肉眼可见。2.2 JSP 页面骨架把公共部分抽出来别每个页面复制导航栏JSP 最容易被写烂的地方就是每个页面都复制一遍 header 和 sidebar。正确做法是用% include file %静态包含或jsp:include动态包含。静态包含在编译期合并适合导航栏这种不常变的动态包含在运行期请求适合需要传参的。!-- /common/header.jsp 抽出来的公共头 -- % page contentTypetext/html;charsetUTF-8 languagejava % % // 从 session 取当前用户没登录直接踢回登录页 Object loginUser session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } % div classtopbar span法律援助与咨询系统/span span当前用户${sessionScope.loginUser.realName}/span /div然后在业务页面里这样引入% page contentTypetext/html;charsetUTF-8 languagejava % % 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里要带useUnicodetruecharacterEncodingutf8否则中文案情描述存进去变问号jdbc.driver用com.mysql.cj.jdbc.DriverMySQL 8.0 之后旧驱动类名已废弃。查询时用PreparedStatement占位符别拼字符串这是防 SQL 注入的底线。3. 申请提交与审核流转JSP 表单到 MySQL 的完整链路怎么写3.1 表单提交的编码问题与文件上传处理法律援助申请往往要上传身份证、低保证明等附件。纯 JSP Servlet 处理文件上传需要commons-fileupload和commons-io两个 jar。表单必须设enctypemultipart/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?errnoFile); 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 { ListFileItem items upload.parseRequest(req); MapString, 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 status1 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 ListLegalApply pageQuery(int pageNo, int pageSize, int status) { ListLegalApply 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 MapString, 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.EntryString, 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。列表展示时要显示“张三的工伤申请李律师回复……”。新手容易先查咨询表再循环查用户表N1 问题。一条 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 contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %JDBC URL 加useUnicodetruecharacterEncodingutf8建库用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不是providedprovided 的包不会打进 war。5.3 现象修改 JSP 后刷新页面没变化原因Tomcat 默认对 JSP 有编译缓存或者浏览器缓存了旧页面。开发阶段频繁遇到。解决Tomcat 的conf/web.xml里把development设为truemodificationTestInterval设为0让每次请求都检查 JSP 是否改动。浏览器侧用 CtrlF5 强制刷新或者开 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 暴力破解。换成BCryptjbcrypt一个 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 才定位到是并发覆盖。那之后我养成的习惯是凡是状态会变的表要么加乐观锁要么加日志最好两个都加。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?