做公共法律服务平台这个选题,不是因为它热门,而是它把很多 JavaWeb 的基础点都串起来了。SpringBoot、MyBatis Plus、MySQL、Redis、JWT、Vue,这些技术在毕业设计里几乎是标配,但真正要把一个类似“智慧法务在线服务与咨询系统”的需求落地,还得老老实实走一遍需求分析、表结构设计、状态流转、权限控制这些步骤。这篇文章把我从零到一实现这个项目的过程、关键代码、踩过的坑,以及答辩时常被追问的细节都拆开讲,准备选同类型题目的同学可以直接拿来当参考。
1. 项目概述与需求拆解
1.1 项目定位与核心需求解析
公共法律服务平台本质上是一个信息中介和业务流程管理平台。普通用户需要找律师做法律咨询,申请法律援助;律师需要接收咨询单、回复问题、管理案件;管理员需要审核律师资质、审核援助申请、维护法律资源。只做一个静态页面显然不够,必须有一整套用户体系、业务流转数据和权限控制。
我从需求分析开始,把项目拆成在线法律咨询、法律援助申请、法律资源管理、律师入驻审核、后台数据统计五个功能域。标题里提到的“智慧法务在线服务与咨询系统”,核心就是把咨询、援助、资源这三个主要业务信息化;而“数字化法律援助与资源管理平台”强调的是审核、指派、资源沉淀这些管理操作。这个定位决定了系统不是简单的 CMS,而是有明确状态流转的业务系统。
需求分析阶段最容易犯的错是一上来就写代码。我做这个项目时先列了用户故事,比如“作为普通用户,我希望发布一个法律咨询单后能看到律师接单进度”“作为律师,我希望系统只推送平台审核通过后的咨询单”。把这些用户故事整理成功能清单,再倒推数据库表,后面编码会顺利很多。
1.2 用户角色与核心业务流程
系统主要有三类角色,他们的操作边界差异很大。普通用户是求助者,律师是服务提供方,管理员是审核方。我用一张表把角色、核心操作和权限边界固定下来:
| 角色 | 核心操作 | 权限边界 |
|---|---|---|
| 普通用户 | 发布咨询、回复咨询、申请法律援助、查看资源 | 只能管理自己的数据,不能审核、不能指派律师 |
| 律师 | 接单、回复咨询、查看指派给自己的援助案件 | 不能编辑资源、不能查看其他律师的私密咨询 |
| 管理员 | 审核律师入驻、审核援助申请、指派律师、维护资源分类 | 不能代替用户发起咨询、不能干预具体咨询内容 |
核心业务闭环可以用一条流程串起来:用户注册登录,浏览公开的法律资源;遇到具体问题后发布咨询单,律师接单并回复;如果符合法律援助条件,用户可以填写援助申请,管理员审核后指派律师,律师进行线下服务,最后用户在线上确认结果。
此外还有一条辅助流程是律师入驻。普通用户申请成为律师时,需要提交姓名、执业证号、专业领域、执业年限,管理员审核通过后,该用户才会在角色上拥有律师权限。这里的核心技巧是:不要把“律师”简单做成一个布尔字段,而要把“申请-审核-通过”这个过程记录下来,方便管理员追溯。
2. 技术选型与整体架构设计
2.1 后端框架选型:SpringBoot 的取舍
我选择 SpringBoot 而不是传统 SSM 框架,最关键的原因是 SpringBoot 把 Spring 的很多配置自动化了,内嵌 Tomcat,打一个 jar 包就能跑,对毕业设计来说部署和演示都非常方便。更重要的是,SpringBoot 生态很完整,MyBatis Plus、Spring Data Redis、JWT 这些组件都能快速整合,不用像 SSM 那样写一堆 XML 配置。
也有同学问为什么不直接用 Spring Cloud 微服务。做这个项目时我认真想过,单机应用场景下拆微服务纯粹是增加复杂度。公共法律服务平台的数据一致性要求高,比如审核指派、律师接单,一旦拆成多个服务,事务和部署难度都会上升,答辩时反而容易被评委追问“你这个场景有必要用消息队列吗”。技术选型要匹配业务规模,这也是一个值得在论文里写的观点。
2.2 技术栈清单与关键依赖
完整的后端技术栈如下:
| 技术 | 版本参考 | 主要作用 |
|---|---|---|
| Spring Boot | 2.7.14 | 后端基础框架,自动配置,内嵌Tomcat |
| MyBatis Plus | 3.5.3 | ORM框架,提供BaseMapper和分页插件 |
| MySQL | 8.0 | 业务数据持久化 |
| Redis | 2.x(Spring Data Redis) | 缓存登录状态或验证码,提升热点资源访问速度 |
| JWT | 0.11.5 | 登录令牌签发与校验 |
| Lombok | 1.18.x | 减少实体类getter/setter代码 |
| Hutool | 5.x | 加密、日期处理等工具集 |
前端用的是 Vue 3 + Element Plus + Axios,通过 Vite 构建。前后端分离是现在比较主流的做法,也是这个题目比较合理的呈现方式。Maven 关键依赖片段如下:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>这里特别注意 JWT 依赖版本。很多老教程还在用 0.9.1,那个版本在 JDK8 以上容易出现jjwt依赖冲突,而且新版 API 变化比较大。用 0.11.5 之后,Jwts.builder()和Jwts.parserBuilder()的用法更清晰,答辩讲起来也更有底气。
2.3 整体架构与项目目录规划
系统采用前后端分离架构,后端提供 RESTful 接口,前端通过 Axios 调用接口。后端目录按职责分层,每一个包只干一件事,这样答辩时能清晰地讲出每一层的职责,后续加功能也容易定位。
后端项目的代码结构如下:
src/main/java/com/legal/platform ├── controller # 接口层 ├── service # 业务逻辑层 ├── mapper # 数据访问层 ├── entity # 实体类 ├── dto # 请求和响应对象 ├── common # 统一返回结果、异常处理、常量 ├── config # MyBatis Plus、Redis等配置 └── security # JWT工具、拦截器、注解前端目录相对简单,views按模块放页面,api里统一封装请求方法,router配置路由和导航守卫。路由守卫很重要,比如没有管理员权限的用户访问后台页面时,前端要直接跳转到 403 页面,而不是等后端接口报错。
整体架构上我没有做 Nginx 负载均衡,因为单机部署足够。但我在前端打包后使用 Nginx 托管静态页面,再把后端接口通过反向代理转发到 8080 端口,这样演示时只需要暴露一个 80 端口,体验更接近真实上线环境。
3. 核心功能设计与数据模型
3.1 数据表规划与关联关系
数据库设计是毕业设计的重头戏,也是评委会盯着看的部分。我先梳理实体关系:用户与律师资料是 1 对 1,用户与咨询单是 1 对多,咨询单与回复是 1 对多,用户与法律援助申请是 1 对多,法律援助申请与律师是 多 对 1。
核心表规划如下:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| sys_user | 用户账号表 | username、password、role、status |
| lawyer_profile | 律师资料扩展表 | user_id、real_name、license_no、specialty、audit_status |
| consultation | 咨询单表 | user_id、lawyer_id、title、content、status |
| consultation_reply | 咨询回复表 | consultation_id、from_user_id、content |
| legal_aid_apply | 法律援助申请表 | user_id、apply_type、case_desc、status、assign_lawyer_id |
| legal_resource | 法律资源表 | title、category、content、view_count |
建表时一定要加create_time和update_time,这是 MyBatis Plus 自动填充的好帮手,也可以作为论文里“时间维度审计”的素材。咨询单表的核心建表语句我写在这里:
CREATE TABLE consultation ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT '提问用户ID', lawyer_id BIGINT DEFAULT NULL COMMENT '接单律师ID', title VARCHAR(100) NOT NULL COMMENT '咨询标题', content TEXT NOT NULL COMMENT '咨询内容', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待受理 1已接单 2已回复 3已关闭', is_public TINYINT DEFAULT 1 COMMENT '是否允许公开展示', create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, KEY idx_user_id (user_id), KEY idx_status (status) );这里我没有设置外键,而是用索引来关联业务关系。实际项目中使用物理外键会导致删除、更新很麻烦,也不利于分库分表,所以通过逻辑外键来维护关系是更常见的实践。这一点可以在论文的数据设计部分专门说明。
3.2 认证授权与角色权限设计
认证授权这块,我没有直接引入 Spring Security,而是用 JWT 加自定义拦截器实现。这样做的原因有两个:第一,需求里只涉及三种固定角色,不需要动态权限模型;第二,自己实现拦截器能把“登录态校验-角色判定”讲得非常清楚,答辩时更容易展示你对权限控制的理解。
用户登录成功后,后端签发一个 JWT,里面放入userId和role两个自定义声明。后续请求在请求头带上Authorization: Bearer token,拦截器会解析 token,把用户信息放入ThreadLocal,业务代码里通过一个静态方法获取当前登录用户。
密码用 BCrypt 加密存储。BCrypt 每次生成的哈希值不同,同一密码在不同时间的密文都不一样,可以有效防止彩虹表破解。即使数据库泄露,攻击者也没办法直接还原明文密码。这是安全设计中必须写进文档的一点。
角色权限通过注解和拦截器配合完成。我自定义了一个@RequireRole注解,标注在 Controller 方法上。拦截器在进入方法前先判断当前用户角色是否匹配,如果不匹配直接返回 403。比如:
@GetMapping("/admin/audit/list") @RequireRole("ADMIN") public Result auditList(...) { return Result.ok(); }这种写法的好处是权限控制和业务代码分离,看代码的人一眼就能知道接口是否限制角色。前端侧再配合路由守卫控制页面跳转,形成双重防护。
3.3 咨询与法律援助状态机设计
状态机是公共法律服务平台里最容易讲出亮点的设计点。咨询单不能随意从“待受理”跳到“已关闭”,必须按照业务流程逐步流转。我用状态字段加操作接口的方式,把合法流转路径固定下来。
咨询单状态如下:
| 状态值 | 含义 | 可执行操作 |
|---|---|---|
| 0 | 待受理 | 律师接单,用户取消 |
| 1 | 已接单 | 律师回复,用户回复,关闭 |
| 2 | 已回复 | 继续回复,关闭 |
| 3 | 已关闭 | 无 |
法律援助申请状态如下:
| 状态值 | 含义 | 可执行操作 |
|---|---|---|
| 0 | 待审核 | 管理员审核通过/驳回 |
| 1 | 审核通过 | 管理员指派律师 |
| 2 | 已指派 | 律师确认接收,用户查看结果 |
| 3 | 已完成 | 无 |
| 4 | 已驳回 | 用户重新申请 |
为什么一定要用状态机?因为如果普通用户直接调用接口把自己的申请改成“已完成”,业务流程就乱了。后端的每一次修改都必须校验前置状态,比如“指派律师”只能在“审核通过”状态下执行。我在代码里统一封装了状态变更服务,避免散落在各个 Controller 里出现逻辑漏洞。
4. 核心模块实现与实操要点
4.1 登录认证与 JWT 拦截器
登录模块是整个系统的入口,实现起来并不复杂,但容易忽略细节。用户提交用户名和密码后,后端从数据库查出用户,用 BCrypt 校验密码,成功后签发 token。
JWT 工具类的核心方法如下:
public String generateToken(Long userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) .signWith(key, SignatureAlgorithm.HS256) .compact(); }登录接口:
@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { User user = userService.lambdaQuery() .eq(User::getUsername, dto.getUsername()) .one(); if (user == null || !PasswordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.error("用户名或密码错误"); } String token = jwtUtil.generateToken(user.getId(), user.getRole()); return Result.ok().put("token", token).put("role", user.getRole()); }拦截器在preHandle中解析 token,如果 token 非法或过期,直接返回未登录的错误码。同时要放行登录接口、公共资源查询接口等白名单路径。这里有一个容易被忽视的点:需要处理前后端跨域问题。如果前端和后端不在同一个端口,请求会被浏览器拦截,因此需要配置CorsFilter,允许前端地址跨域访问。
4.2 在线咨询与回复模块
在线咨询是这个平台的核心模块,也是并发问题最明显的地方。用户发布咨询单以后,平台可能会推送给多个律师,但一个咨询单只能被一个律师接走。如果不在数据库层面做并发控制,两个律师同时点击接单,就有可能都成功,造成重复指派。
解决这个问题最可靠的做法不是先把数据查出来判断状态,再执行更新,而是直接用带条件的更新语句:
boolean updated = consultationService.update( new LambdaUpdateWrapper<Consultation>() .eq(Consultation::getId, id) .eq(Consultation::getStatus, 0) .set(Consultation::getStatus, 1) .set(Consultation::getLawyerId, currentUserId) ); if (!updated) { return Result.error("该咨询单已被其他律师接单"); }这段代码的核心在于WHERE status = 0。即使两个请求同时到达,数据库的行锁也会保证只有一个更新语句影响行数为 1,另一个更新影响行数为 0,从而被判断为失败。这是“乐观锁”思想的一种实现方式,不需要额外加版本号字段,也比先查询再更新更安全。
用户和律师的回复记录都写入consultation_reply表,插入回复后需要把咨询单状态改成“已回复”。回复操作必须加事务,因为要么回复记录插入成功并且状态更新成功,要么两者都不变。使用@Transactional可以保证这一点。常见问题是事务不生效,很多情况是同类中方法直接调用导致代理失效,解决办法是把事务方法放到另一个 Service 类中,或者通过注入自身代理对象调用。
4.3 法律援助申请与审核模块
法律援助申请模块涉及实名信息和材料上传,我把用户身份证号码和手机号做了脱敏处理,在后台列表中默认显示中间四位打码。用户提交申请时,可以选择类型,比如民事援助、劳动仲裁援助等,并上传证明材料图片。
申请流程中,文件上传是一个实操重点。我使用本地磁盘存储上传文件,把物理路径保存到数据库,文件访问通过虚拟路径映射。前端上传时限制文件类型和大小,后端也要再次校验。如果我写的是MultipartFile上传接口,会同时校验文件后缀名和 MIME 类型,防止把恶意文件传到服务器。
管理员审核通过后,需要给申请指派律师。指派操作和律师接单是类似的场景,同样要防止两个管理员同时对同一个申请指派。所以我使用了跟接单一样的条件更新:
boolean assigned = legalAidService.update( new LambdaUpdateWrapper<LegalAidApply>() .eq(LegalAidApply::getId, aidId) .eq(LegalAidApply::getStatus, 1) .set(LegalAidApply::getStatus, 2) .set(LegalAidApply::getAssignLawyerId, lawyerId) );指派成功后,用户可以查看援助进度,律师也能在“我的援助案件”列表中看到分配下来的申请。这里还需要加入一个提醒机制,我使用了简单的站内消息表,在审核通过、指派成功等关键节点生成一条消息通知,比每次都手动查询业务表要符合用户习惯,也让系统看起来更有“智慧”感。
4.4 法律资源管理与前台展示
法律资源模块实现了分类管理、列表分页、详情查看和搜索。管理员在后台维护资源分类,比如“婚姻家庭”“劳动纠纷”“合同债务”,发布资源时使用富文本编辑器,内容以 HTML 形式保存。重点是前端展示时要做好 XSS 过滤,我引入了一个简单的 HTML 白名单过滤器,只允许<p><h2><ul>这类标签通过,避免用户提交恶意脚本。
资源列表使用 MyBatis Plus 分页插件,查询条件包含标题关键字、分类、创建时间。分页插件配置很简单,创建MybatisPlusInterceptor并添加PaginationInnerInterceptor即可。因为题目中“资源管理平台”这个定位,除了增加浏览量,还可以增加“点赞”或者“收藏”功能,丰富系统的可讲内容。
前台页面展示法律资源时,需要把咨询单和资源分开。公开的咨询回复内容如果设置为is_public = 1,可以匿名脱敏展示在类似“普法问答”栏目,这样既能保护用户隐私,又增加了平台内容厚度,是一个低成本高收益的设计。
5. 常见问题与排查技巧实录
5.1 项目启动阶段的坑
我遇到的第一个问题是 MySQL 连接报时区错误:The server time zone value '???ú±ê׼ʱ¼ä' is unrecognized。解决方式是连接串中加上serverTimezone=Asia/Shanghai,同时使用useSSL=false来避免无用的安全警告。
第二个常见问题是 Redis 闪退导致登录验证码接口不可用。如果功能没有强依赖 Redis,最好在配置中允许降级,或者简单一点,先不集成 Redis,用 JWT 本身去维持登录态,也能完成毕业设计核心要求。但如果加了 Redis,答辩时可以讲“热点法律资源缓存到 Redis”,前提是配置和依赖都要调通。
第三个坑是 Maven 依赖冲突。尤其是 JWT 库与老版本 Spring Boot 整合时容易出现NoClassDefFoundError。我建议使用 0.11.5 版本的jjwt-api和jjwt-impl,并在启动后立即测试登录接口,遇到报错直接看Caused by定位是哪个类找不到。
项目启动时端口被占用也是高频问题。Windows 下可以用netstat -ano | findstr 8080找到占用进程,然后到任务管理器结束进程。Linux 下用lsof -i:8080。别直接改端口逃避,因为前端代理和后端配置都要联动修改。
5.2 并发与数据一致性问题
除了接单和指派,用户重复提交表单也会造成脏数据。比如用户快速连续点击“提交法律援助申请”,会插入两条相同记录。解决方法是后端在插入前增加幂等校验,或者在表上增加唯一约束。最直接的方式是前端提交后立刻禁用按钮,后端再根据申请类型、身份证号和时间窗口做一次查重,双保险。
另外要特别留意@Transactional失效的情况。我在调试时发现,同一个类中一个方法调用另一个带事务注解的方法,事务可能不生效,因为 Spring 的 AOP 代理默认是外部调用才生效。例如在ConsultationService的closeConsultation()中直接调用updateStatus(),后者虽然标了@Transactional,但不走代理,事务不会正常回滚。解决方案是把事务方法放到独立的 Service 类中,或者在当前类中注入自身代理。
5.3 答辩前需要准备的关键点
答辩时评委很少会一句一句看代码,更多是围绕设计和技术决策提问。我准备了几个高频问题:
第一,为什么表结构要用逻辑外键而不是物理外键。我会回答:物理外键在插入和删除时开销更大,也限制表结构的调整;逻辑外键搭配数据库索引,足够满足当前系统并发的需求,而且更符合实际项目的规范。
第二,为什么选择自定义 JWT 拦截器而不使用 Spring Security。我会说:当前系统角色少,需求简单,自定义方案能精确控制权限逻辑;如果未来需要复杂权限模型,可以再平滑迁移到 Spring Security,不影响业务层代码。
第三,如何保证数据安全。我会从密码 BCrypt 加密、JWT 过期时间、SQL 预编译、XSS 过滤四个层面分别说明。别只答“用了某某加密”,要讲清楚解决了什么问题。
第四,系统如何部署。我会讲后端打包jar、前端npm run build生成静态资源,再放到 Nginx 下,由 Nginx 反向代理到后端接口。如果评委问“能不能直接访问”,我会把localhost:80的演示地址说明一遍。
5.4 给做这个题目的同学一些额外建议
我个人做完这个项目的最大感受是,先把最核心的闭环跑通再谈扩展。很多同学一开始就想着做在线聊天、视频咨询、智能问答,结果核心的业务流程都没理顺,最后论文拼凑痕迹严重。公共法律服务平台最值得投入精力的是咨询单状态流转和援助指派逻辑,这两个地方能体现系统设计能力。
还有一个小技巧是开发过程中坚持记录问题和解决方案。我建了一个简单的文档表格,每次遇到报错、修复过程、是否复发都记下来。最后写论文时,这些记录直接变成“系统调试与测试”章节的素材,比临时回忆可靠得多。
最后,在合理参考开源项目时要带着问题去看代码,比如“接单这段如果不用条件更新会怎样”“这个接口不加权限会怎样”。只有把别人的设计转化成自己能讲清楚的知识,答辩时才不会心虚。公共法律服务这个方向后续还有很多扩展空间,比如对接第三方电子签章、短信提醒、案例库检索,如果时间允许,挑一个点深入做进去,项目还会有更大的加分空间。