Spring Boot + Vue前后端分离律所案件管理系统开发全解析 1. 项目概述与核心需求拆解1.1 律所管理系统到底解决了什么问题我先把这个项目放在一个真实的场景里聊一聊。你在一个律师事务所里日常办公最头疼的是什么不是打官司而是案件信息的流转和管理。一个律师手头同时跟进七八个案子每个案子的当事人、对方当事人、案由、阶段、开庭时间、收费情况、文书材料全散落在微信聊天记录、Excel表格、纸质卷宗里想找一份材料翻箱倒柜想统计年度收案量还要手动数半天。这套前后端分离Spring Boot律师事务所案件管理系统要解决的就是这类问题。它把律所的核心业务——案件管理、客户管理、收费管理、文档管理、统计报表——从线下搬到了线上做成了一套标准的Web应用。管理者和行政人员能看全局律师能维护自己手头的案子所有数据沉淀在数据库里随时可查可统计。标题里的技术栈很清晰后端用Spring Boot前端用Vue持久层用MyBatis配合MySQL数据库前后端通过RESTful接口通信。这套组合在当前的Java全栈开发里属于非常主流的技术选型无论你是刚学完Spring Boot基础准备做第一个完整项目的新手还是想给自己简历上加一个高质量业务系统的求职者这个项目都值得完整跟一遍。我拆这个项目的时候会尽量把每一步的原理和坑都讲透而不是单纯甩一段源码给你。1.2 功能模块与角色权限设计开发这类管理系统第一件事不是写代码是先分清角色。律师所有三类典型使用者系统管理员负责系统配置和账号管理律师/助理处理具体的案件业务前台或行政人员负责客户接待和收退费登记。我按这个逻辑把系统拆成几个核心模块登录与权限模块基于JWT做身份认证不同角色登录后看到不同的菜单和操作按钮案件管理模块案件的新增、分派、跟进、归档支持按案号、当事人姓名、案由快速检索客户管理模块维护客户的基本资料、历史委托记录做关联查询时能直接看到这个客户名下有几个案子收费管理模块记录律师费、退费、开票状态统计每个案件的收支情况文书管理模块上传起诉状、代理词、判决书等文件和案件关联绑定支持在线预览和下载统计报表模块按时间段、律师、案件类型聚合数据生成受案量、结案率等图表。这里有个很多新人容易忽略的细节——权限校验到底做在前端还是后端正确的做法是两端都做。前端控制菜单显隐只是提升体验后端接口必须做角色校验才行否则别人拿个普通账号直接调接口就能访问管理员数据。后面讲JWT实现时我会专门演示这个逻辑。2. 技术选型与架构设计的底层逻辑2.1 为什么选Spring Boot Vue这套组合市面上做管理系统可以选的技术栈太多了Python的Django/Flask、PHP的Laravel、Go的Gin都能做。但Spring Boot在这类业务系统里优势非常明显尤其是配合MyBatis操作数据库时。Spring Boot的自动配置机制极大简化了项目初始化过程内嵌Tomcat让部署只需要一个jar包Spring Security或JWT整合做权限控制有成熟的生态支持。前端选Vue的理由也很实在。这套系统的主要使用场景是律所内部的电脑浏览器操作密集、交互复杂、需要快速响应。Vue的响应式数据绑定适合处理这种表单密集型页面Element UI或者Element Plus这一套现成的组件库做表格、弹窗、表单校验效率非常高。相比ReactVue对国内开发者更友好中文文档齐全遇到问题搜解决方案也方便。再强调一下MyBatis在这套架构里的定位。一般管理系统会大量涉及多表关联查询、条件动态拼接、统计报表这种灵活多变的SQLMyBatis的XML映射文件给了你直接写SQL的空间遇到复杂需求时不要和框架绕弯子直接写SQL反而最清晰。配合MyBatis-Plus的分页插件列表分页查询几行代码就搞定。这套组合不是性能最强的但一定是业务开发效率最高的之一。2.2 前后端分离的通信与认证机制前后端分离的核心就是两边各管各的前端只管页面渲染和用户交互后端只提供数据接口两者通过HTTP请求通信。这就带来两个必须处理的工程问题跨域和认证。跨域问题解释起来不复杂。前端的开发服务器跑在localhost的某个端口比如8080后端接口跑在另一个端口比如9090浏览器出于安全策略会拦截跨端口请求这就是经典的跨域。解决方式在后端配置跨域过滤器允许指定来源的请求访问接口后面部署章节我会给出完整配置。认证机制我用的是JWT。用户登录成功后后端生成一个包含用户ID和角色的签名Token返回给前端前端把Token存在本地存储里之后每次请求都在HTTP请求头里带上这个Token。后端通过拦截器校验Token的合法性并从中解析出用户信息。这么做的好处是服务端不用存储Session天然适合前后端分离架构也方便以后做负载均衡扩展。Token的过期策略、刷新机制我在实际操作中踩过坑后面专门说。2.3 数据库设计与表关系数据库设计决定了这套系统能走多远我见过太多项目因为表结构没设计好写到后面越改越痛苦。这套系统我建议从这几张核心表说起sys_user用户表存储登录账号、密码、姓名、角色ID密码存MD5加盐后的值sys_role角色表定义管理员、律师、前台等角色case_info案件表核心业务表包含案号、案件名称、案由、受理日期、当前阶段、承办法官、关联客户ID和负责人律师IDcustomer_info客户表客户姓名、联系方式、证件号码、地址等fee_info收费表关联案件ID和客户ID记录收费金额、收费日期、收费方式、开票状态case_document文书表关联案件ID存文件名、文件路径、上传时间、上传人。表关系上customer_info和case_info是一对多一个客户可以委托多个案件case_info和fee_info是一对多一个案件可以有多次收费记录case_info和case_document是一对多一个案件下有多个文书。外键在业务表里通过逻辑字段关联不要真的在数据库层面大批量加物理外键不然删除数据时会被约束卡住。这个经验是生产环境踩坑换来的。建表的SQL里注意几个细节金额字段用decimal(10,2)不要用float否则计算精度会出问题时间字段统一用datetime每个表都加create_time和update_time字段方便后续排查数据和做统计。3. 后端核心实现要点3.1 项目目录结构与分层设计后端项目我建议按标准的Controller-Service-Mapper三层结构组织。Spring Boot项目创建好之后包里大致这样分com.lawfirm.system ├── controller # 接口层接收前端请求 ├── service # 业务层处理业务逻辑和事务 │ └── impl ├── mapper # MyBatis数据访问层 ├── entity # 数据库实体类 ├── dto # 接口传输对象 ├── config # 配置类跨域、拦截器、MyBatis ├── common # 公共类统一返回结果、异常处理、工具类 └── filter # JWT认证过滤器为什么要把entity数据库实体和dto接口传输对象分开因为数据库字段和前端需要的JSON字段经常不一致。比如实体里有密码字段但登录接口的返回对象里绝对不能带密码比如列表查询需要额外返回案件关联的客户姓名而实体里只有customerId。把两者分开各层之间解耦改接口不影响实体结构。以案件新增这个操作举例完整调用链路是这样的前端把表单数据JSON发到CaseController的addCase接口Controller把JSON转成CaseDTOService层将DTO转成CaseInfo实体然后调Mapper的insert方法写入数据库。插入成功后Service层还要处理关联操作——比如给案件生成一个自定义格式的案号、给客户分配默认跟进人。这些逻辑放在Controller里会很臃肿放在Service层是最合适的。3.2 MyBatis整合与XML映射文件配置Spring Boot整合MyBatis很简单引入mybatis-spring-boot-starter依赖然后在application.yml里配置数据源和Mapper扫描。我给出一个生产可用的配置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/lawfirm?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: yourpassword jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.lawfirm.system.entity configuration: map-underscore-to-camel-case: true这里有两个关键配置。第一map-underscore-to-camel-case设置为true后数据库下划线字段customer_id能自动映射到实体类的驼峰属性customerId不需要每个字段都手写ResultMap除非遇到多表关联查询的复杂返回结构。第二mapper-locations指向XML文件目录所有动态SQL写在XML里比注解方式更利于维护和调试。写XML映射文件时有个高频场景是条件动态查询。案件列表需要一个支持多条件筛选的查询案号模糊匹配、客户姓名匹配、当前阶段精确匹配、负责人律师匹配其中任意一个条件都可能为空。这种SQL在MyBatis里用 标签加 判断实现select idselectCaseList resultTypecom.lawfirm.system.entity.CaseInfo SELECT ci.*, cus.customer_name FROM case_info ci LEFT JOIN customer_info cus ON ci.customer_id cus.id where if testcaseNo ! null and caseNo ! AND ci.case_no LIKE CONCAT(%, #{caseNo}, %) /if if testcustomerName ! null and customerName ! AND cus.customer_name LIKE CONCAT(%, #{customerName}, %) /if if testcaseStage ! null and caseStage ! AND ci.case_stage #{caseStage} /if if testlawyerId ! null AND ci.lawyer_id #{lawyerId} /if /where ORDER BY ci.create_time DESC /select标签会自动去掉第一个条件前面的AND这个细节如果你手动拼WHERE很容易写错。这种写法查询条件组合非常灵活前端传哪些参数就拼哪些条件一个Mapper方法能应对列表页的所有筛选场景。3.3 基于JWT的登录认证与权限控制JWT的完整流程是用户提交用户名密码后端校验通过后生成Token返回给前端前端存储并在后续请求头里携带后端通过拦截器验证Token决定是否放行。核心代码分三步第一步登录接口生成Token。用jjwt库版本用0.9.1这个版本对初学者最友好。生成逻辑String token Jwts.builder() .setSubject(user.getUsername()) .claim(role, user.getRole()) .claim(userId, user.getId()) .setExpiration(new Date(System.currentTimeMillis() 72 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();这里有个设计细节值得说Token有效期设72小时律所场景下用户不会频繁登录有效期太短会让人烦躁设太长又有安全风险。自己练习时可以短一点生产环境按公司安全规范来一般8小时到24小时比较常见。第二步写一个拦截器在Spring Boot里实现HandlerInterceptor接口重写preHandle方法。从请求头取Token解析失败就返回401状态码解析成功就把用户信息放到request的attribute里供后续业务逻辑使用。第三步在WebMvcConfigurer里注册拦截器并配置放行路径。登录接口和静态资源不需要认证其余接口全部拦截registry.addInterceptor(jwtInterceptor) .addPathPatterns(/**) .excludePathPatterns(/api/auth/login, /error);权限控制还要做到角色级别的粒度。我在Service层的查询方法上加个判断如果当前登录人是管理员可以查全部案件如果是普通律师只能查自己名下或自己参与的案件。这个逻辑放在后端非常重要前端不管怎么藏按钮后端不校验等于白做。4. 前端核心实现要点4.1 Vue工程搭建与目录组织前端推荐用Vue CLI或者Vite创建工程。我说一下Vite的方式现在新项目用Vite更多启动和打包速度都比之前的Webpack方案快不少。创建命令就一行npm create vuelatest按提示选择Vue Router、Pinia、ESLint这些选项。创建的工程里src目录按下面这种方式组织src ├── api # 接口请求封装 ├── router # 路由配置 ├── store # 全局状态管理 ├── views # 页面组件 ├── components # 公共组件 ├── layout # 主布局侧边栏顶栏内容区 └── utils # 工具函数组件库我建议用Element Plus表格、表单、弹窗、菜单这些组件开箱即用和Vue 3配合得很好写管理系统的效率比手写原生HTML快好几倍。安装命令npm install element-plus在main.js里全局注册import ElementPlus from element-plus import element-plus/dist/index.css app.use(ElementPlus)4.2 Axios封装与接口调用前后端分离项目里前端所有请求都要走Axios但直接在每个组件里写axios.get会造成大量重复代码。正确做法是封装一个统一请求工具统一处理基础URL、Token携带、响应拦截和错误提示。在utils/request.js里import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带Token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) // 响应拦截器统一处理错误 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default request这里有个关键决策开发环境通过Vite的代理把/api开头的请求转发到后端地址避免跨域问题。生产环境则用Nginx统一转发。所以前端代码里不用写死后端IP和端口一律以/api开头这个习惯在团队协作时尤其重要换环境部署不用改前端代码。4.3 动态路由与菜单权限前面说过不同角色看到的菜单不同实现方式有两种。第一种最简单写死在路由配置里用meta.roles标注每个页面哪些角色能访问路由守卫里判断用户角色是否匹配。第二种是动态路由用户登录后根据角色从后端获取可访问的菜单和路由表动态添加到路由实例里。第二种扩展性更强适合这套系统——未来给某个角色新加页面权限直接改数据库就行不用重新发版。动态路由的核心逻辑在router/index.js里配合Pinia实现。登录成功保存用户信息和角色后在路由守卫里根据角色动态注册对应模块的路由。需要注意动态添加路由时要调用router.addRoute()并且每次页面刷新后路由会重置所以刷新时要重新拉取菜单信息。这个坑我实际开发时踩过刷新后白屏排查半天发现是路由没重新注册。5. 完整部署教程5.1 本地环境准备与项目启动先说环境要求这套系统本地跑起来需要的软件清单如下软件版本建议用途JDK1.8或11运行Spring BootMaven3.6以上管理后端依赖MySQL5.7或8.0数据存储Node.js16以上运行前端工程IDEA社区版即可开发工具数据导入很简单用Navicat或命令行执行项目里的lawfirm.sql脚本这一步会创建数据库和全部表结构还会写入一个默认管理员账号。执行SQL之前记得手动创建数据库编码用utf8mb4CREATE DATABASE lawfirm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;后端启动流程IDEA打开后端工程等待Maven下载完依赖修改application.yml里的数据库连接密码然后运行主类Application。只要日志里出现Started Application in x.xxx seconds就说明启动成功了。前端启动流程命令行进入前端工程目录先执行npm install装依赖这一步在国内网络环境下会慢一些可以换成国内的npm镜像源。依赖装完执行npm run dev看到Local: http://localhost:5173/就说明启动成功。浏览器打开这个地址用默认账号admin/admin登录。5.2 前后端构建打包开发调试用npm run dev部署上线则不同。后端打jar包很简单在项目根目录执行mvn clean package -DskipTests第一次打包会花几分钟下载依赖打包成功后target目录下会出现一个lawfirm-system-0.0.1.jar。运行这个jar包只需要一行命令java -jar target/lawfirm-system-0.0.1.jar前端打包命令是npm run build打包完成后dist目录里就是纯静态文件部署时让Nginx把请求指向这个目录即可。这里必须提一个生产环境部署的关键问题前端打包后怎么访问后端接口答案是配置Vite的build.proxy在生产环境不生效所以打包后的前端代码里的/api请求必须由Nginx转发到Spring Boot服务上。Nginx配置示例server { listen 80; server_name lawfirm.example.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }关键点在于try_files后面要加/index.html否则刷新某个路由时Nginx会返回404因为前端的Vue Router用的是History模式页面切换不经过服务端。5.3 生产环境部署方案生产部署我推荐用最简单的方式服务器上装好MySQL和Java环境把jar包扔上去用nohup后台运行。步骤就三条上传jar包到服务器例如/opt/lawfirm目录执行nohup java -jar lawfirm-system-0.0.1.jar app.log 21 把前端dist目录上传到Nginx的html目录改好配置重启Nginx。不建议在服务器上打包直接在本地打好包再上传减小服务器压力。服务器安全组要放开80端口Nginx和3306端口MySQL但3306一定不要对公网开放数据库只允许本机连接这是基本的安全底线。数据库迁移到服务器时有两种常用方式一种是导出SQL文件再导入服务器数据库另一种是用Navicat的数据传输功能直接同步。数据量不大时两种都行我习惯用mysqldump导出再导入可控性更强mysqldump -u root -p lawfirm lawfirm_backup.sql服务器上执行mysql -u root -p lawfirm lawfirm_backup.sql6. 常见问题与排查技巧实录6.1 跨域问题前端启动后发现请求后端接口报CORS错误浏览器控制台显示Access-Control-Allow-Origin。原因就是开发环境的Vite端口和后端端口不一致。解决办法有两层第一层是后端加全局跨域配置类允许前端地址访问第二层是前端配置Vite代理让/api请求走代理转发。我建议开发阶段优先配置Vite代理这样前端代码里不发任何绝对地址全都用/api开头代理配置在vite.config.js里server: { proxy: { /api: { target: http://localhost:9090, changeOrigin: true } } }ChangeOrigin设置为true会把请求头里的Host改成目标地址有些后端框架对Host有校验时尤其重要。6.2 MyBatis相关坑MyBatis最常见的坑是日期类型的比较。在XML里写时间范围查询时直接用大于小于符号会报错要用转义字符。比如查创建时间晚于某个时间点if teststartTime ! null AND create_time gt; #{startTime} /if另一个高频问题是多表查询返回字段映射不正确。private String customerName死活查询不出来排查步骤先看SQL直接在数据库执行有没有结果再看实体类有没有加对应属性最后看resultType是否配置正确。这三个环节逐级排查基本都能定位。6.3 数据库连接问题连接MySQL报Communications link failure九成情况是MySQL没启动或者连接地址里的端口写错。还有一类常见报错是Unknown database说明application.yml里的数据库名和实际创建的不一致。这些问题优先级排查方式先看MySQL服务状态再看连接配置最后看防火墙。使用MySQL 8.0时还有一个特殊点驱动类名和旧版不同要用com.mysql.cj.jdbc.DriverURL里必须加serverTimezone参数否则时区报错。6.4 前后端联调问题汇总前端请求返回304 Not Modified这是浏览器缓存导致的。调试接口时把浏览器开发者工具Network选项里的Disable cache勾上避免拿到旧的缓存结果误判代码问题。前端页面刷新后登录状态丢失这是Token只存在内存里导致的。解决方案是把Token存到localStorage请求拦截器每次从localStorage取值。但localStorage有XSS风险生产环境建议用httpOnly的Cookie方案这个复杂一些自己练习时用localStorage够用。每次改完前端代码页面不刷新这是Vite的HMR热更新没有生效。检查一下是不是把代码写在node_modules目录里了——我见过有人把源码放错位置导致热更新失效。源码必须写在src目录下Vite才会监听文件变化。7. 写在最后破事里的经验我从这个项目里学的最大教训是做管理系统数据库设计花的时间永远不算浪费。你花一个下午把表结构想清楚后续每个模块写起来都会顺畅你要是没想清楚就开写后面每加一个功能都要回头改表改表又影响实体类、SQL和前端页面牵一发动全身。另一个经验是关于调试的。前后端分离项目出问题时第一步永远是用Postman或者直接浏览器访问后端接口确认接口返回是否正常。很多新手遇到页面显示不对先在前端代码里翻半天最后发现是后端接口数据就没查出来。定位问题的顺序应该永远是后端接口 → 前端请求 → 页面渲染从数据源到展示层逐段排查而不是倒着来。这套系统后续还可以加不少东西案件时间线功能、任务提醒、卷宗二维码扫码归档、对接钉钉或企业微信的消息通知。做成什么样取决于真实使用场景的需求技术方案都是现成的遇到需求往上加功能就行。希望这篇文章能让你不止把代码跑起来还明白每个设计背后的为什么。