
系统上线那周我记得特别清楚。某律所的行政负责人跟我说他们找案件材料还在靠翻聊天记录当事人在微信里问一句“我的案子现在到什么阶段了”助理就得去翻邮箱、翻文件夹、问主办律师一个回复可能要半小时。也正是那一次我下定决心把“前后端分离Spring Boot律师事务所案件管理系统”完整整理成一套可以直接跑起来的项目。这套系统用的是 Spring Boot Vue MyBatis MySQL典型的前后端分离架构。业务上覆盖了案件登记、当事人信息管理、案件进度跟踪、文档归档、费用记录和角色权限控制这几个律所日常最高频的模块。对于正在做管理系统类开发的同学、需要给律所做信息化方案的小团队或者想找一个完整前后端分离项目来练手的人来说这套系统的设计和代码都是一份可以直接参考、甚至直接改吧改吧拿来用的材料。这篇文章我会把整套系统的拆解思路、表结构设计、后端核心逻辑、前端对接细节和部署流程全部过一遍。重点不是把所有代码贴一遍而是把“为什么这样做”讲清楚包括我自己踩过的坑。1. 案件管理系统的需求边界先搞清律师到底要什么再谈功能很多开发者在拿到“律师事务所管理系统”这个需求时第一反应是把功能堆得足够多电子签章、OCR识别、自动文书生成、复杂计费引擎……结果做了三个月还在做需求分析。我自己最深的体会是律所信息化的第一步不是炫技而是把案件生命周期的“信息孤岛”打通。1.1 从Excel和微信群里长出来的痛点律所的日常管理比多数人想象中原始得多。合伙人手上有几十个案子每个案子的材料分散在个人邮箱、微信文件、本地文件夹甚至纸质档案袋里。助理要汇报进度得先去问律师律师在外开庭客户端 APP 上查不到任何信息;更别说费用到期、开庭日期变更这些时间敏感的事基本靠人脑提醒。这些痛点汇总下来本质就是四个字信息集中。案件的基本信息、当事人、承办律师、时间节点、文档材料如果能集中在一个系统里并且让不同角色看到不同范围的数据那么上面这些场景至少能解决八成。所以我做这套系统的第一个原则就是不做大而全只把案件生命周期的主线跑通——从咨询登记、案件受理、办理过程到结案归档。主线通了其他功能都是后续的锦上添花。1.2 功能模块的优先级怎么排我把这套系统的功能拆成三个优先级后面所有表结构和代码都是按照这个优先级推进的。优先级模块核心功能P0案件管理案件登记、案件列表、案件详情、状态变更P0当事人管理原告/被告/委托方信息维护与案件关联P0用户与权限登录认证、角色区分、数据权限隔离P1文档管理案件材料上传、下载、按案件归档P1日程与节点开庭时间、到期日、里程碑提醒P1费用管理收费记录、阶段费用、发票信息登记P2统计分析案件数量统计、胜诉率、律师工作量报表这里特别想提醒一句权限控制要放到 P0 里做不要拖到最后再补。律所的数据非常敏感合伙人和授薪律师能看到的数据范围本来就该不一样。如果先做完案件 CRUD 再加权限你会发现很多查询接口已经写死了返回全部数据回头改造的成本远大于一开始就设计好。1.3 这套系统的边界哪些事不做明确不做什么比做什么更重要。这套系统里我没有做在线审批流、电子签章、邮件自动归档这样重逻辑的功能。原因很简单这些功能一旦引入复杂度是指数级上升的而且律所内部通常已经有自己的 OA 或专业工具强行再造轮子没有意义。系统的定位是“律所内部案件协作平台”接住的是日常最琐碎的信息管理需求。等这套跑顺了后面完全可以再开发当事人端的查询门户或者对接企业微信做消息提醒——这些扩展点在表结构设计阶段就要预留好。2. 技术选型的底层逻辑Spring Boot Vue MyBatis MySQL 为什么够用这个技术组合被用了太多次以至于很多人觉得它是“默认选项”而不是“合理选项”。但实际从律所管理系统这个场景出发这四样东西刚好卡在最合适的点上。2.1 前后端分离而不是传统模板渲染如果只是做一个内部管理系统用 Spring Boot 直接返回 Thymeleaf 页面、或者用 JSP技术上完全行得通。但我在评估时发现了一个关键变量这个系统的前端大概率不会被只做一次。律所的需求会很频繁地调整页面布局还要考虑以后做当事人查询门户Portal如果前后端耦合在一起每次改版都要把整个应用重新部署风险太大。前后端分离之后后端只提供 JSON 接口前端是独立的 Vue 工程两边可以并行开发、独立部署。这个“多端复用的可能性”就是选前后端分离的真正理由而不是因为“大家都这么写”。2.2 MyBatis 和 JPA 之间的取舍在 Spring Boot 生态里数据访问层有 JPA/Hibernate 和 MyBatis 两条路。这套系统我坚定地选了 MyBatis核心原因是律所的案件统计和管理报表SQL 逻辑比想象中复杂得多。举一个很典型的例子统计“某个律师名下所有未结案案件中按案件类型分组的数量”。用 JPA 写起来要靠 JPQL 或者 Criteria API可读性很差一旦关联三四张表调试成本非常高。而 MyBatis 可以直接写原生 SQLSQL 长什么样、怎么走索引开发者一清二楚。另外一个很多人忽略的点是 MyBatis 的typeHandler在处理枚举和状态字段上的优势。案件状态、当事人类型这些字段在数据库里我存的是 TINYINT 数字在 Java 里是枚举在 JSON 输出里是带中文描述的文本。用 MyBatis 的枚举 typeHandler可以在写入和读取时自动转换避免了到处手动转换。2.3 前端到底选 Vue 2 还是 Vue 3这套系统的管理端我用了 Vue 3 Element Plus。原因很直白新项目没必要再选一个已经进入维护期的 Vue 2 生态。虽然网上大量历史教程还在用 Vue 2 Element UI但新写的项目如果图省事选旧技术后面组件库的 bug 修复和安全公告都得自己扛。Vue 3 的组合式 API 对管理系统这种“大量表单 复杂交互逻辑”的页面来说代码组织确实比 Options API 清爽。比如一个案件详情页需要同时管理案件基本信息、当事人列表和日程记录用setup语法可以很清楚地把这几块逻辑拆开而不是全部塞进data/methods里。2.4 MySQL 的定位和边界MySQL 在这套系统里就是老老实实存结构化业务数据。案件信息、当事人、费用记录、系统用户全是典型的关系型数据事务是刚需。这一点上 MySQL 的 InnoDB 引擎完全够用。有人可能会问案件文档上传之后附件是不是要用对象存储我在这套系统里用的是服务器本机磁盘存储数据库只存文件路径。文档量到了一定规模之后可以再加对象存储比如云上的 OSS 或者 MinIO接口层面只需要改一个文件存储 Service 的实现对上层业务没有任何影响。这个“存储策略隔离”的设计我建议你在写代码时也保持。3. 数据库设计跟着案件生命周期走表结构自然就清晰了很多初学者拿到项目第一件事就是写实体类、写 Mapper这是反的。我自己的习惯是先画表表的结构搞清楚了业务逻辑是不可能乱的。这套系统的数据库一共设计了大概二十来张表但核心主线只有四张案件表、当事人表、案件-当事人关联表、里程碑进度表。其他的用户、角色、菜单、文档、费用都是围绕这条主线展开。3.1 案件表所有业务的中心案件表是整个系统数据模型的心脏。我的设计如下字段名类型说明idBIGINT主键case_noVARCHAR(32)立案编号格式如 LN20250108-001case_titleVARCHAR(255)案件名称或事由case_typeTINYINT案件类型民事/刑事/行政/非诉等用枚举case_statusTINYINT状态待受理/办理中/已结案/已归档court_nameVARCHAR(128)受理法院或仲裁机构judge_nameVARCHAR(64)承办法官可空case_amountDECIMAL(12,2)诉讼标的额owner_lawyer_idBIGINT承办律师用户IDaccept_timeDATETIME受理时间close_timeDATETIME结案时间remarkVARCHAR(512)备注create_by / create_time / update_by / update_time—审计字段全表通用这里有两个细节值得展开。第一为什么 case_status 用 TINYINT 而不是 VARCHAR因为状态是机器要判断逻辑的字段用数字存储、用枚举类统一管理比存中文“办理中”要可靠得多。中文一旦在数据库里被改成“办理 中”或者“办里中”整条状态判断就废了。MyBatis 配合枚举 typeHandler对外展示时自动映射成中文完全不冲突。第二case_no 的编号规则要提前设计。律所的案件编号不只是给人看的还要作为检索关键词。我用的是LNYYYYMMDD-三位流水号同一天内从 001 开始递增。这个编号在插入时需要防并发重复做法是取当天的最大编号加一再加一个唯一索引兜底。3.2 当事人和关联关系天然多对多别偷懒一个案件里原告可能不止一人被告可能是多个公司还会存在第三人。所以当事人和案件的关系一定是多对多。我的表设计是client_info当事人主表。存姓名/单位名称、身份证号/统一社会信用代码、电话、地址、当事人类型自然人或法人。case_client关联表。字段包括 id、case_id、client_id、client_role原告/被告/第三人/委托方、is_primary是否为案件主要联系人。这里有一个从实际业务中总结出的教训当事人必须做成主数据不能直接把姓名、电话存在案件表里。同一家公司可能和律所有多个案子如果每次都在案件表里冗余一份客户信息后面一旦客户改了联系地址所有历史案件的公司信息都是旧的根本改不过来。3.3 里程碑表案件进度不是“改个状态”就完了一个案件从受理到结案中间会经过起草起诉状、立案、开庭、判决、执行等若干个节点。单纯在案件表上改一个 status 字段只能回答“案件到哪个阶段了”回答不了“这个案子最近发生过什么”“下一步要提醒谁”。所以我设计了case_milestone表字段说明id主键case_id案件IDmilestone_type节点类型立案/开庭/判决/执行等milestone_name节点名称如“第一次开庭”plan_time计划时间用于日程提醒actual_time实际发生时间handler_id经办人remark节点说明这个表同时承载了两个重要功能一是案件详情页里的“时间线”从这张表取数二是“待办提醒”把当前状态为待处理、且计划时间在最近三天内的节点查出来推给承办律师。3.4 权限相关表RBAC 模型是现成的用户权限我采用了经典的 RBAC基于角色的访问控制模型五张表sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu。用户表里有个字段需要注意dept_id也就是部门/团队ID。律所的权限往往不只按角色切还要按团队切。比如 A 团队主办律师只能看到 A 团队的案子主任或管理员可以看到全部。这个我放在后面的后端数据权限部分细说。4. 后端核心设计认证、状态流转和数据权限是三大硬骨头Spring Boot 后端的写代码顺序建议按照“基础设施 - 通用能力 - 业务逻辑”展开。如果一上来就写案件 Controller到后面接入登录时你可能会想摔键盘。4.1 统一响应体和全局异常处理前后端分离的项目后端接口返回格式必须统一否则前端没法封装 Axios 拦截器。我定义了统一的ResultT结构public class ResultT { private Integer code; // 0 成功非0失败 private String message; // 提示信息 private T data; // 真正的数据 }配套的还有一个全局异常处理器用RestControllerAdvice把业务异常、参数校验异常、系统异常分别转换成对应的Result。这一点上很多项目做了一半就放弃了这里异常返回一种格式、那里又返回另一种格式前端拦截器里到处是兼容逻辑。我的经验是所有返回必须走同一个 Result包括登录失败这种异常情况。前端只需要看 code 就知道请求是不是正常。4.2 登录认证和 Token 处理这套系统我没有引入 Spring Security原因是它的配置链对很多刚接触前后端分离的人来说反而是一道极高的门槛。基于这个项目“内部系统 少量角色”的定位我用拦截器 JWT 实现了完整的认证授权项目结构更轻也更容易看懂。流程是用户提交用户名密码后端校验通过后生成 JWTtoken里面带上用户ID、用户名和角色编码。前端把 token 存到 localStorage每次请求在 Header 里带Authorization: Bearer xxx。后端的拦截器拦截所有/api/**请求从 Header 里解析 token。解析成功后把用户信息放到ThreadLocal里后续 Service 层可以直接拿当前用户。核心代码大致是这个样子public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BusinessException(未登录或登录已过期); } LoginUser user JwtUtil.parseToken(token.substring(7)); UserContext.set(user); // 存在 ThreadLocal return true; } }这里有一个我在实际项目里踩过的坑拦截器配置的路径一定要避开登录接口本身否则用户还没登录就被拦截器拦住了形成死循环。我在WebMvcConfig里明确配置了放行路径列表至少包含/api/auth/login。4.3 案件状态流转用装填了边界条件的动作方法去改状态案件的状态字段如果被随意 UPDATE数据迟早会乱。比如一个已经“已结案”的案件因为某次误操作被改回了“办理中”这在律所是不可接受的。所以我把状态变更收敛到几个业务动