基于Spring Boot+Vue的律师事务所案件管理系统设计与实现
干了这几年Java后端接过的管理类项目少说也有七八个。这次整理的是一个律师事务所案件管理系统前后端分离后端用 Spring Boot MyBatis前端用 Vue数据库 MySQL。不是那种只跑通接口的教学 Demo而是能真正填业务数据、能部署上线、能在浏览器里操作的完整系统。文章里我会把整体设计思路、核心表结构、后端接口实现、前端页面组织、部署过程中踩过的坑全部过一遍顺便把源码的目录结构讲清楚给正在做案件管理、OA 系统或者任何前后端分离项目的朋友一个参考。1. 项目概述与需求拆解1.1 为什么做这个系统以及前后端分离的取舍律师事务所的日常管理绕不开几个痛点案件信息散落在 Excel 表格里不同律师各记各的新人接手看不到全貌案件进度靠人工催办审限期限一过就容易出风险文档版本管理混乱同一份诉状改了几版最后不知道哪份是定稿。所以案件管理系统的核心价值就是把“信息孤岛”变成“一条主线”从收案登记、材料归档、期限提醒到结案统计全部串起来。我在技术选型上直接定了前后端分离。核心原因有两个第一前端展示和交互迭代太快今天要加个看板明天要调整表单布局如果塞在 JSP 或者 Thymeleaf 里每次改模板都要重新部署后端太痛苦第二现在的团队分工已经模块化了有人专门写 Vue有人专门写接口分离以后可以并行开发互不阻塞。当然分离也带来了跨域、鉴权、部署复杂度上升的问题但这些问题都有成熟解法后面我会说。这个系统适合谁参考一句话如果你正在做案件类、审批类、项目管理类业务系统这篇文章里涉及的表设计、状态流转、权限控制和部署方式都可以直接搬过去改改用。尤其是学生和准备转行的朋友代码里没有花里胡哨的炫技全部是面试和实际工作里最常用的写法。1.2 核心功能模块清单这类系统功能不用贪多但关键环节一定要打通。我拆成了六个模块客户管理维护自然人客户和法人客户记录联系方式、证件信息、关联案件列表。案件管理案件的收案登记、承办律师分配、当前状态流转、审限日期管理、结案归档。文档管理案件相关的起诉状、合同、证据材料上传支持按案件维度查看和下载。日程提醒开庭日期、续期日期、审限到期前的自动提醒用待办消息推给承办人。费用管理记录律师费、诉讼费、差旅费支持按案件汇总统计。统计分析按案件类型、状态、律师维度统计立案数、结案数用简单列表展示不搞复杂图表。这里要注意案件模块是核心其他模块都围绕它展开。设计的时候我优先把案件表的关系理清楚客户、文档、费用、日程全部通过 case_id 关联这样查询不会乱。1.3 技术选型不堆新框架用顺手的三件套我见过不少人做项目一上来就 Spring Cloud、Redis、RabbitMQ结果业务逻辑还没写就死在环境搭建上。这个系统我刻意压了技术栈后端就是 Spring Boot 2.7 MyBatis MySQL前端就是 Vue 2.7 Element UI Axios权限用 JWT部署用 Nginx。为什么这么压Spring Boot 2.7 是目前稳定性和文档最平衡的版本3.0 改动过大不少老项目的依赖还没跟上MyBatis 虽然写 XML 有点繁琐但复杂查询和 SQL 调优直接可控比 JPA 上手速度快面试也常问Vue 2.7 有 Options API 和模板语法对大多数后端转前端的人特别友好社区里 Element UI 组件也一把梭。其实一开始也想过直接拿若依框架改但若依内置的权限体系虽然完善项目里很多字段和流程是定制化的自己搭反而更好控制也不会有一堆用不到的代码。2. 数据库设计一张表一个坑2.1 案管系统的核心表结构数据库设计是这种管理系统最耗时间的部分。我先把核心表列出来大家看关联关系表名说明关键字段t_customer客户表id, customer_name, customer_type, phone, id_card, create_timet_case案件表id, case_no, case_name, case_type, status, owner_user_id, customer_id, register_date, deadline_datet_case_document案件文档表id, case_id, file_name, file_path, upload_user_id, upload_timet_schedule日程表id, case_id, title, schedule_time, remind_time, remind_flagt_fee_record费用表id, case_id, fee_type, amount, create_timesys_user用户表id, username, password, real_name, role_idsys_role角色表id, role_name, role_code设计时我特意避开case这个表名因为它是 MySQL 的保留字直接用会报语法错误或者需要不停加反引号。这种细节特别耽误时间第一次写 SQL 的时候踩过坑后面就养成习惯表名统一加t_前缀。每张表我都建议加上create_time、update_time和逻辑删除标志deleted。案件和文档这类数据不能物理删除万一误删了证据材料恢复成本很高。逻辑删除字段设为tinyint查询时默认加where deleted 0前端删除接口走的也是 UPDATE不是 DELETE。2.2 案件期限与状态流转怎么设计案件系统里最核心的业务概念有两个状态和期限。状态决定当前案件在哪个环节期限决定催办提醒的触发时机。我的状态设计不是用简单的 String 字段到处塞而是用一组固定的状态码1待收案2办理中3已安排开庭4已结案5已归档。注意状态不要用中文用数字或者英文字符串。前端展示时再通过字典映射成中文后端逻辑判断只认编码。这样后续如果状态增加不需要改数据库表只要改字典和判断逻辑。期限字段分成两个register_date立案日期deadline_date审限到期日。系统提醒的逻辑很简单就是每天跑一次定时任务找出deadline_date在三天内且状态不是4和5的案件生成提醒消息。案件办结后deadline_date的提醒自动失效因为查询条件里已经过滤了状态。2.3 初始化数据与字符集选择数据库初始化我直接提供init.sql里面除了建表语句还有默认管理员账号、角色字典数据和状态字典数据。角色就两个管理员、律师。律师只能看自己和本部门案件管理员看全部。字符集这里要特别提醒数据库、表、字段的字符集统一用utf8mb4不要用utf8因为utf8在 MySQL 里存不了 emoji 表情。现实中当事人姓名、地址、备注经常有生僻字和特殊符号utf8mb4才能稳妥放下。排序规则用utf8mb4_general_ci大小写不敏感查询体验更友好。还有一个非常容易忽略的点创建数据库时指定default character set utf8mb4 collate utf8mb4_general_ci而不是建表时再去纠结字段级别。建议直接用我提供的 SQL 文件里边的建库建表语句都调好了。3. 后端实现Spring Boot MyBatis 的落地细节3.1 接口分层与统一返回值后端代码分层我严格按 Controller、Service、Mapper 三层来。Controller 只做参数接收和路由定义不写业务逻辑Service 层处理业务规则、事务和异常Mapper 层只负责 SQL 交互。这样做的直接好处是如果后面要加 Redis 缓存或者消息推送只要改 ServiceController 不用动。接口设计上用 RESTful 风格但不过度追求资源化。比如获取案件列表用GET /api/case/list新增案件用POST /api/case/save删除案件用DELETE /api/case/{id}。统一返回结构写在ResultT里属性包含code、message、data成功返回200失败返回自定义错误码。前端 Axios 拦截器统一处理code ! 200的情况弹错误提示不用每个页面单独写一遍判断。我这里放一段 Controller 核心代码大家感受一下RestController RequestMapping(/api/case) public class CaseController { Autowired private CaseService caseService; GetMapping(/list) public ResultPageResultCaseVO list(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size, CaseQuery query) { return Result.success(caseService.queryPage(page, size, query)); } PostMapping(/save) public ResultLong save(RequestBody CaseDTO dto) { return Result.success(caseService.saveCase(dto)); } GetMapping(/detail/{id}) public ResultCaseDetailVO detail(PathVariable Long id) { return Result.success(caseService.getDetail(id)); } }特别说明一点列表接口的参数我用了一个CaseQuery对象来接收比散落的 RequestParam 干净得多后续加筛选条件只需要在对象里加字段。3.2 MyBatis 映射文件里的动态 SQL列表查询是这类系统最常用的接口基础功能包括按案件名称模糊查询、按案件类型精确查询、按状态查询、按承办律师查询、按时间范围查询。如果每个条件都写死一条 SQL最后会拼出十几个if分支维护起来很崩溃。我的做法是在 XML 文件里用 MyBatis 的动态 SQL 统一处理select idqueryPage resultTypecom.demo.entity.CaseEntity SELECT * FROM t_case where if testcaseName ! null and caseName ! AND case_name LIKE CONCAT(%, #{caseName}, %) /if if testcaseType ! null and caseType ! AND case_type #{caseType} /if if teststatus ! null AND status #{status} /if if testownerUserId ! null AND owner_user_id #{ownerUserId} /if if teststartDate ! null AND register_date gt; #{startDate} /if if testendDate ! null AND register_date lt; #{endDate} /if /where ORDER BY register_date DESC LIMIT #{offset}, #{pageSize} /select注意动态 SQL 里的if标签每个条件都要做非空判断否则用户不选任何条件时WHERE后面跟着空字符串会导致 SQL 错误。MyBatis 的where标签会自动处理第一个AND关键字这个机制用熟了会省很多事。关于 MyBatis 映射还有一个坑数据库字段是下划线命名Java 属性是驼峰命名需要在application.yml里开启驼峰映射mybatis: configuration: map-underscore-to-camel-case: true不开启的话deadline_date永远映射不到deadlineDate属性上而且不会报错只会返回 null排查半天才发现。这个配置我建议所有用 MyBatis 的项目都写上。3.3 定时任务做案件到期提醒期限提醒功能我用的 Spring 自带的Scheduled注解没有引入 Quartz。对于这种轻量级扫描任务Spring 自带的足够用。核心逻辑是每天的早上 8 点扫描一次把未来三天内到期的案件批量生成提醒记录。代码大概长这样Component public class DeadlineRemindTask { Autowired private ScheduleService scheduleService; Scheduled(cron 0 0 8 * * ?) public void remindDeadline() { scheduleService.generateDeadlineRemind(); } }注意启动类上要加EnableScheduling注解否则定时任务不会生效这又是一个“代码没问题但就是不动”的常见原因。生成提醒时我通过案件状态过滤掉已结案的记录否则办结案件还会天天顶在待办列表里非常影响体验。3.4 事务与并发控制案件保存接口是整个系统最核心的写操作涉及案件主表新增、案件状态变更、文档记录写入、费用记录初始化必须保证在同一事务里。Service 方法上我加了Transactional默认传播行为是 REQUIRED任何一个子操作失败整个事务回滚。还有一个细节审限日期和案件编号的生成要保证唯一。案件编号我用“当前年月日 随机数”的方式比如LS20250612001但直接用时间加随机数在并发高的时候可能重复。稳妥的办法是数据库唯一索引兜底或者用一张专门的自增序号表。因为单点系统并发不大我就用 Redis 的 INCR 生成每日序号但为了不额外引入中间件最终在数据库层面加了唯一索引来兜底。这种“双保险”的思路大家在真实项目里可以直接复用。4. 前端实现Vue 的页面组织与权限控制4.1 路由设计与刷新404问题前端项目用 Vue Router 管理页面路由分两块静态路由login、home、dashboard业务路由caseList、caseDetail、customerList、documentList、feeList。登录成功后前端把 JWT token 存在 localStorage每次请求在 Axios 拦截器里加Authorization头。一个经典问题是用 history 模式部署到 Nginx刷新子路由页面会报 404。因为 Nginx 只配置了根路径指向 index.html刷新/case/list时它会去磁盘找这个路径的文件找不到自然 404。解决办法是 Nginx 配置里加一行 try_fileslocation / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; }这样刷新任意子路由都会回退到 index.html由前端路由重新接管页面。新手上线最容易碰到这个问题我每次部署都会习惯性检查这行配置。4.2 页面组件复用与案件表单处理案件相关页面很多结构是重复的比如客户信息选择、案件类型下拉、日期选择器。我用 Element UI 的el-form配合自定义组件来减少重复开发。案件类型和状态用字典接口从后端加载下拉选项来自后端配置表这样前端不用写死状态码对应的中文。表单校验是一个重点。案件名称、承办律师、立案日期是必填审限日期不能早于立案日期。Element UI 的表单校验规则写在data里el-form-item的prop对应字段。这里要提醒一句规则里的trigger建议写成[blur, change]不然填完日期不离开输入框校验可能不触发。案件保存的逻辑我单独放在src/api/case.js里import request from /utils/request export function getCaseList(params) { return request({ url: /api/case/list, method: get, params }) } export function saveCase(data) { return request({ url: /api/case/save, method: post, data }) }所有 API 统一从request.js走而不是在组件里直接写 axios。这样拦截器、错误提示、token 注入都集中管理。4.3 跨域与Axios封装前后端分离必然遇到跨域。开发环境我通过 Vue 脚手架里的 devServer proxy 解决module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }生产环境用 Nginx 反向代理解决前端dist里的请求路径统一是/apiNginx 把/api转发到后端端口。这两种方式都能规避 CORS 的复杂配置而且保持了前端请求代码的一致性。Axios 封装这里我把 token 注入、响应错误码处理、401 跳登录页都写在拦截器里service.interceptors.request.use(config { const token window.localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { router.push(/login) } return Promise.reject(error) } )后端 JWT 里存了用户 ID 和角色码每次请求经过拦截器校验登录接口返回 token其他接口通过RequestHeader取值。5. 部署上线从0到能跑起来的完整过程5.1 环境准备JDK、MySQL、Node 的版本坑部署上线这一步环境能折腾掉你半天时间。我先说版本JDK 用 1.8 或 11Spring Boot 2.7 都能跑MySQL 建议装 8.0如果之前一直用 5.7最明显的差别是驱动配置和时区问题。MySQL 8.0 的连接驱动类是com.mysql.cj.jdbc.DriverURL 里要带serverTimezoneAsia/Shanghai不然报时区错误。Node 环境只需用 16 或 18 稳定版版本太高或者太低npm install 可能报错。前端构建时如果遇到node-sass安装失败直接换成sass包也就是dart-sass兼容性好得多。Vue 项目我自己日常开发都用 yarn 或者 npm但部署时建议用 npm因为 CI 环境里 npm 不需要额外安装。5.2 后端打包与启动后端打包很简单进入项目根目录执行mvn clean package -DskipTests打包完成后target目录下会生成一个lawsuit-system.jar。启动命令是java -jar lawsuit-system.jar --spring.profiles.activeprod生产环境配置我放在application-prod.yml里数据库地址、用户名、密码、文件上传路径都抽离出来。特别强调一点不要把密码直接写在 yml 文件里提交到 Git哪怕这是个学习项目也要养成用环境变量注入的习惯。启动时报错大多是端口占用或者数据库连不上查日志第一眼先看堆栈最下面的 Caused by别被一堆框架日志带偏了。5.3 前端构建与Nginx部署前端构建同样简单npm install npm run build构建产物在dist目录下。把整个dist目录上传到服务器的/usr/share/nginx/html然后配置 Nginxserver { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意proxy_pass后面这个斜杠很重要proxy_pass http://127.0.0.1:8080;会保留原始 URIproxy_pass http://127.0.0.1:8080/;会去掉/api前缀再转发。我这边后端接口路径本来就带/api所以不加斜杠。5.4 常见问题速查表问题现象可能原因解决办法前端调用接口报 CORS error开发环境没有配 proxy或生产环境 Nginx 未转发按上文配置代理或 Nginx location刷新子路由 404Nginx 未配置 try_files加try_files $uri $uri/ /index.html;MySQL 报时区错误JDBC URL 缺少 serverTimezoneURL 加?serverTimezoneAsia/Shanghai中文乱码数据库字符集不是 utf8mb4统一库/表/连接字符集案件保存接口返回 500事务方法内部有字段非空校验失败查看日志 Caused by定位具体 SQL日期字段映射为 nullMyBatis 驼峰映射未开启配置map-underscore-to-camel-case: true定时任务不执行启动类缺少EnableScheduling加注解并确认 cron 表达式正确另外再提一嘴 MyBatis 的二级缓存。如果你的项目开启了二级缓存案件列表这种高频查询确实快但案件一旦被修改缓存刷新不及时用户会看到旧数据。我的做法是默认不开二级缓存只在字典表这类几乎不变的数据上使用。别一上来就迷信缓存管理系统的瓶颈多数在数据库索引和接口设计不在缓存。如果你以后要加监控告警可以引入 Spring Boot Admin它能看到内存、线程、健康状态接口调用出现慢请求能快速定位。但这是锦上添花的事先确保业务闭环跑通再说。最后分享一个扩展思路。案件系统的核心是“案件状态 期限 角色权限”这个模型可以平移去做合同管理、法务审批、审计项目管理。只要把t_case换成对应业务实体再调整一下状态流转节点整个后端架构和前端的列表、详情、表单模式几乎不用大改。我自己写完这套以后另一个项目上线时基本是复制过去改了改字段省了很多时间。如果你按同样思路搭自己的系统建议先花时间把数据库表关系和状态机理清这是地基地基稳了后面怎么盖楼都不慌。