做律所的案子管理系统最怕的不是需求复杂而是“把案件当流水账记”。很多同行拿着Excel表跟了几年案子等到合伙人问“这个月有几个待开庭的纠纷”“哪个律师手上压案最多”的时候数据翻半天也拼不齐。所以我一直在想能不能用一套轻量的前后端分离方案把律所案件从收案、分案、跟案到归档的完整链路管起来。这次我要分享的正是一个基于SpringBootVueMyBatisMySQL的律师事务所案件管理系统完整源码加部署教程从数据库设计到前端交互到上线发布把整个思路和坑都捋一遍。如果你是刚学完SpringBoot和Vue不知道怎么串起来或者律所内部想搞一套内部web系统这篇文章可以少走很多弯路。1. 项目整体设计与技术选型思路1.1 为什么选择前后端分离架构律所案件管理系统天然需要多人协作前台行政录入案件律师查看自己的任务合伙人要看全局统计不同角色在不同终端上操作。如果用传统JSP模板渲染前端页面逻辑和后端Java代码耦合太深每次改个按钮样式都要重启应用碰上复杂一点的案件时间线展示更是痛苦。前后端分离之后后端只负责提供JSON接口前端用Vue做单页应用两者通过HTTP协议通信。这样有几个实际好处一是前端和后端可以并行开发前端写页面的时候后端只要把接口约定好用Mock数据就能跑起来二是以后想换客户端比如做个内部小程序或者桌面工具后端接口完全可以复用三是部署的时候前端静态资源可以扔到Nginx上后端单独跑SpringBoot应用互不干扰排查问题也更清晰。我实际做的时候采用了比较经典的“三端分离”结构Vue工程独立管理SpringBoot工程独立管理数据库用MySQL保存业务数据。中间通过RESTful API对接认证用JWT Token后续扩展其他系统对接也比较方便。1.2 技术栈选型为什么是SpringBoot Vue MyBatis MySQL这套组合现在已经是国内JavaWeb项目的主流标配了不是没有理由的。SpringBoot简化了Spring配置内嵌Tomcat使得一个main方法就能启动Web服务。对于律所管理系统这种中等规模业务来说SpringBoot自带的自动配置、Starter依赖、Actuator监控能省掉大量XML配置时间。我选择SpringBoot 2.7.x版本稳定且兼容度好不用去踩SpringBoot 3.0的Jakarta迁移坑。Vue我用的是Vue 3 Vite Element Plus的组合。Vue的响应式机制和组件化开发非常适合表单密集、交互频繁的管理系统Element Plus的Table、Form、Tree组件做案件列表和权限树可以说是开箱即用。相比ReactVue的中文文档和社区生态更友好团队上手成本低。MyBatis虽然JPA在简单CRUD上很爽但案件管理系统的查询往往涉及多表关联、动态条件、分页统计MyBatis的SQL可以手工精细化控制配合XML里的if标签做动态SQL能写出很灵活的查询。而且律所项目通常有大量报表统计需求用MyBatis直接在SQL里写统计逻辑比在内存里过滤效率高得多。MySQL免费、普及率高5.7版本的InnoDB引擎对事务支持可靠。案件系统涉及金额、当事人隐私信息必须保证ACID事务MySQL配合Spring的Transactional能很好满足。1.3 项目目录结构与功能模块划分为了不让自己后续被一堆文件淹没工程结构必须一开始就规划好。后端采用controller → service → mapper三层结构另外单独抽出common和config包存放通用工具和配置。大致结构如下law-case-system ├── frontend # Vue前端工程 │ ├── src │ │ ├── api # 接口请求封装 │ │ ├── components # 通用组件 │ │ ├── router # 路由配置 │ │ ├── stores # 状态管理Pinia │ │ └── views # 页面组件 │ └── package.json └── backend # SpringBoot后端工程 ├── src/main/java/com/example/lawcase │ ├── controller # 接口层 │ ├── service # 业务层 │ ├── mapper # MyBatis持久层 │ ├── entity # 实体类 │ ├── dto # 数据交互对象 │ ├── common # 统一结果/异常/工具 │ └── config # 配置类 └── pom.xml功能模块上我拆成了这几个核心部分案件管理收案、分案、跟进、归档全生命周期、客户信息管理、律师及账户管理、任务提醒待办事项、统计报表、系统日志。配合前端动态路由可以按角色显示不同菜单。2. 数据库设计与MyBatis核心细节2.1 案件核心表结构设计数据库设计是整个系统质量的地基尤其是律所场景一个案件涉及客户、对方当事人、承办律师、费用、时间节点等多个维度。我设计的最核心几张表如下。案件主表 case_info字段名类型说明idbigint主键自增case_novarchar(32)案件编号如LAS202400001case_namevarchar(128)案件名称case_typetinyint案件类型民事/刑事/行政/非诉case_statustinyint状态1收案、2分案、3跟进、4归档client_idbigint委托人/客户IDopponent_namevarchar(64)对方当事人lawyer_idbigint承办律师IDassign_timedatetime分案时间close_timedatetime结案时间create_timedatetime创建时间update_timedatetime更新时间客户表 client_info记录委托人的姓名、电话、证件号、地址等注意敏感字段要做脱敏处理尤其是导出Excel时不能把完整身份证号暴露出去。费用明细表 case_fee关联case_id记录费用类型代理费、差旅费、保全费等还有应收、实收、开票状态等字段。这块和财务对账强相关金额字段用decimal(12,2)避免float精度问题。任务提醒表 task_remind关联case_id和lawyer_id记录任务内容、截止日期、完成状态前端可以按律师和日期范围过滤待办事项。设计表的时候要重点考虑索引。案件系统的查询大部分围绕律师ID、案件状态、创建时间这几个维度所以我在case_info上建立了idx_lawyer_status联合索引和idx_create_time普通索引费用表在case_id和status上建索引。别小看这些索引一次统计数据全表扫描会瞬间拖垮后端接口尤其是案子一多接口响应直接从毫秒级变成秒级。2.2 MyBatis映射与动态SQL实战MyBatis的XML映射文件是这个项目里最见真功夫的地方。比如案件列表查询我需要支持多条件组合律师ID、案件类型、状态、时间范围。如果不用动态SQL要么写多个Mapper方法要么拼SQL字符串都很蠢。MyBatis的whereif标签能完美解决。select idselectCasePage resultTypecom.example.lawcase.entity.CaseInfo SELECT * FROM case_info where if testlawyerId ! null AND lawyer_id #{lawyerId} /if if testcaseType ! null AND case_type #{caseType} /if if testcaseStatus ! null AND case_status #{caseStatus} /if if teststartTime ! null AND create_time gt; #{startTime} /if if testendTime ! null AND create_time lt; #{endTime} /if /where ORDER BY create_time DESC /selectwhere标签会自动处理第一个条件前的AND避免出现WHERE AND lawyer_id ?的尴尬。注意时间比较的和在XML中需要转义成gt;和lt;或者用![CDATA[ ]]包裹。这个问题在MyBatis里踩的人很多不转义的话XML解析直接报错。分页我用的是PageHelper插件一行PageHelper.startPage(pageNum, pageSize)接着执行Mapper方法就能返回分页数据。PageHelper的原理是拦截器在执行SQL前自动拼接LIMIT语句。要注意的是startPage方法必须紧跟查询语句中间不能再执行其他SQL否则分页会失效。除了基本查询案件统计报表也是MyBatis的强项。比如统计每个律师的办案数量select idcountCaseByLawyer resultTypemap SELECT lawyer_id, COUNT(*) AS case_count FROM case_info WHERE case_status 4 GROUP BY lawyer_id /select这类统计查询用resultTypemap可以免去创建DTO简单场景下非常高效。2.3 TypeHandler与通用字段自动填充案件表里有不少枚举状态我既不想在Java和数据库之间反复转换又不想每个实体都写一堆getter判断。MyBatis的TypeHandler帮我解决了这个问题。比如案件状态caseStatus用Integer存数据库在Java里我想用枚举CaseStatus类型就自定义一个TypeHandlerMappedTypes(CaseStatus.class) public class CaseStatusTypeHandler extends BaseTypeHandlerCaseStatus { Override public void setNonNullParameter(PreparedStatement ps, int i, CaseStatus parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.getValue()); } Override public CaseStatus getNullableResult(ResultSet rs, String columnName) throws SQLException { return CaseStatus.fromValue(rs.getInt(columnName)); } // 其他getNullableResult重载省略 }然后在Mapper.xml的resultMap里指定typeHandler即可。这样做的好处是实体类里的状态字段就是枚举类型业务代码写起来更安全也能避免魔法数字散落各处。通用字段填充create_time、update_time我一开始每个表都手动insert后来发现太容易遗漏。改用MyBatis的Insert注解加自定义拦截器统一填充。SpringBoot中使用MyBatis提供的MetaObjectHandler可以拦截insert和update操作自动填充公共字段。这样一来任何实体只要继承一个BaseEntity写入时时间戳就自动带上无需每个方法自己处理。提示MyBatis的一级缓存默认是SqlSession级别的二级缓存默认关闭。查询结果在同一个SqlSession内可能被缓存对于系统内大量的实时性查询开启二级缓存前一定要考虑数据一致性。我在这个项目里没有开启二级缓存因为律所数据准确度要求极高不希望出现脏读。3. 后端核心功能实现与接口规范3.1 SpringBoot快速搭建与统一响应封装后端我用Spring Initializr生成基础工程依赖选了Spring Web、MyBatis Starter、MySQL Driver、Lombok、JWT相关库。一个非常实用的习惯是所有接口返回统一的结构体ResultT。Data public class ResultT { private Integer code; // 200成功其他为失败 private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(String message) { ResultT r new Result(); r.setCode(500); r.setMessage(message); return r; } }所有Controller都返回Result前端axios拦截器统一处理code这样不需要每个请求单独判断HTTP状态码全局错误弹窗一次搞定。同时定义全局异常处理器用RestControllerAdvice捕获业务异常、参数校验异常和兜底异常避免把堆栈信息直接暴露给前端。3.2 案件生命周期管理与状态机案件不是简单的CRUD它有明确的流转状态收案录入基本信息、分案分配给律师、跟进记录进展、提交文档、结案归档生成结案报告、锁定数据。如果放任前端随便改status字段会出现把已归档的案子重新置回收案状态的荒唐事。我在后端用了状态机模式定义一个简单接口public interface CaseState { boolean canTransferTo(int nextStatus); }然后在每个状态枚举里实现合法流转规则。例如分案状态只允许流转到跟进或回退到收案归档状态不允许再向任何状态流转。所有更新案件状态的接口都判断当前状态和目标状态是否匹配不合法直接抛业务异常。这套设计虽然前期多写了点代码但后期维护非常省心尤其多名行政人员同时操作时能有效避免状态错乱。3.3 登录鉴权与权限控制JWT RBAC律所系统里律师和合伙人的权限边界必须严格。我用JWT做无状态认证SpringSecurity做资源权限控制。大致流程用户登录成功后服务端生成包含用户ID、角色、过期时间的JWT Token前端存在localStorage中。每次请求在Authorization头带上Bearer xxx后端通过过滤器解析Token并设置当前用户上下文。权限模型采用RBAC用户表、角色表、菜单权限表、用户角色关联表。在SpringSecurity层面给不同接口配置hasRole()或hasAuthority()。比如/api/case/delete只有管理员角色可以调用普通律师只能查看和处理自己的案件。需要说明的是JWT天然无法主动失效如果律所人员离职Token还没过期就存在安全隐患。我的处理是把Token的过期时间设置短一些比如4小时再结合Redis记录用户可用的session状态每次请求校验Token有效后还要查询Redis中是否存在当前会话。这套方案弥补了JWT不可控的问题实际项目中很推荐。3.4 文件上传与MinIO集成案件处理过程中会有大量扫描件、证据材料、律师函文档。直接把文件存到数据库既不现实也存在数据库膨胀问题。我集成了MinIO来管理文件。MinIO是开源对象存储接口兼容AWS S3部署简单非常适合自建存储。在SpringBoot中集成MinIO只需要引入MinIO Java SDK配置端点、账号密码、桶名称。文件上传接口先校验文件扩展名和大小然后生成唯一的对象名称如case/2024/06/xxx.pdf上传后把可访问的URL保存到案件附件的关联表中。这里要注意如果桶的访问权限是私有需要提供预签名URL给前端下载。预签名URL可以设置有效期比如30分钟这样既安全又能避免文件长时间暴露。我实际遇到的坑是MinIO的地址不能用localhost尤其是在服务器上前端和后端不在同一台机器时上传路径的回调地址若不正确浏览器会直接报跨域错误。所以配置必须统一使用服务器的内网IP或公网域名具体看部署环境。3.5 关键接口设计与分页参数约定在定义接口时我遵循RESTful风格但也没钻牛角尖。比如案件列表我用POST /api/case/page而不是GET /api/case?page1size10因为列表查询条件多用POST可以把筛选条件放到Body里避免URL太长。接口返回的分页数据结构统一为{ total: 128, records: [ { caseId: 1, caseName: xxx } ] }前端用Element Plus的el-table配合el-pagination数据格式刚好对齐不用再做额外的数据转换。参数校验上日期范围类的字段后端一定要做校验防止开始时间大于结束时间导致SQL查不出数据还浪费资源。4. 前端Vue3实现与前后端联调4.1 Vue工程初始化与动态路由前端我用Vite创建Vue3工程组件库选Element Plus。动态路由这个功能对权限控制至关重要不同角色登录后应该能看到不同的菜单和页面。实现方式是登录成功后获取该用户拥有的菜单列表通过router.addRoute()动态添加路由。const modules import.meta.glob(../views/**/*.vue) function addDynamicRoutes(menus) { menus.forEach(menu { const component modules[../views/${menu.component}.vue] router.addRoute(Layout, { path: menu.path, name: menu.name, component: component }) }) }这里用到Vite的import.meta.glob进行懒加载避免打包时把全部页面一次性打包。注意动态添加路由后需要调用router.replace()跳转到目标页面否则刷新后页面会空白。4.2 Axios请求封装与异常拦截axios封装是前端项目必要的“基建”。我在请求拦截器里自动附带JWT Token在响应拦截器里统一处理code码。service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } // 401 跳转登录页 if (res.code 401) { router.push(/login) } ElMessage.error(res.message || 系统异常) return Promise.reject(new Error(res.message)) }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )需要注意的是文件下载类的接口需要设置responseType: blob这种情况响应拦截器就不能直接判断code要特殊处理。我一般把下载接口独立封装避免拦截器统一逻辑破坏二进制流。4.3 案件管理核心页面组件拆解案件列表页是我花时间最多的页面。整个页面结构分为筛选区、操作按钮区、表格区、分页区。筛选区包含案件编号、类型、状态、日期范围表格列包含案件编号、名称、客户、承办律师、状态、创建时间操作列根据状态显示“编辑”“跟进”“归档”按钮。为了复用我把状态标签封装成CaseStatusTag组件输入状态值自动显示对应的颜色和文字。例如el-tag :typestatusColor v-ifstatus 1收案/el-tag案件表单页则大量使用了Element Plus的表单校验。比如案件必填字段的rules配置其中日期范围使用DatePicker的value-formatYYYY-MM-DD方便向后端传字符串。部分页面涉及客户信息的身份证号脱敏显示在列表里只显示前3后4详情页可以查看完整信息但需要权限。这些都是从真实业务需求提炼出来的点做系统时最容易忽略等到法务或合伙人提需求再加就麻烦。4.4 前端与后端集中联调思路前后端联调中最磨人的就是接口字段不一致。我联调时采用一个简单粗暴的规范前端所有请求都通过src/api/目录下的模块函数导出每个接口一个函数参数和后端DTO保持一致。这样如果后端改了字段名前端只需改一个地方而不是到处搜fetch。跨域问题在开发环境下用Vite代理解决。在vite.config.js中配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端发请求写/api/case/page代理会转发到后端8080端口规避了浏览器同源策略。生产环境则统一由Nginx反向代理保证前后端在同一个域名下这是最合理的方案。5. 完整项目部署教程从零到上线5.1 环境准备JDK / MySQL / Node / Maven先把服务器环境捋清楚。JDK我用的1.8虽然新版有17甚至21但SpringBoot 2.7.x配合JDK8最稳尤其兼容性方面基本不会出幺蛾子。MySQL版本用的5.7这里有个关键点MySQL8的默认认证插件是caching_sha2_password如果你的MySQL驱动版本比较老连接时会报Public Key Retrieval is not allowed。所以要么把认证插件改成mysql_native_password要么用8.x对应的连接驱动并配置allowPublicKeyRetrievaltrue。我给客户部署时统一用MySQL5.7规避这一堆麻烦。Node环境用来构建前端。Node版本建议14以上我用的16。Maven 3.8配置国内镜像源后依赖下载会快很多。5.2 后端打包与启动后端打包需要先修改application.yml中的数据库连接、MinIO配置、JWT密钥等为生产环境参数。然后执行mvn clean package -DskipTests打包成功后在target目录下生成lawcase-0.0.1-SNAPSHOT.jar文件。启动方式很简单nohup java -jar lawcase-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod lawcase.log 21 用nohup后台启动日志输出到文件。这里我建议再加-Xms512m -Xmx1024m设置初始和最大堆内存避免默认值过高或过低。启动后检查lawcase.log看到Started LawCaseApplication就说明成功了。如果不想直接用jar也可以在服务器上装Tomcat把SpringBoot项目打成war包放到webapps目录。SpringBoot内嵌容器本身就可以跑外部Tomcat方式更适合已有统一运维体系的团队。打成war包需要在pom里设置packagingwar/packaging并且启动类继承SpringBootServletInitializer。但我的建议是既然用了SpringBoot就尽量使用内置Tomcat部署更简单省掉很多版本兼容的麻烦。5.3 前端构建与Nginx部署前端构建前先检查src/api里的baseURL是否配置成了相对路径/api不要写死成http://localhost:8080否则部署到服务器上必挂。然后执行npm install npm run build构建完成后dist目录下就是静态资源。在Nginx配置中把dist目录作为站点根目录server { listen 80; server_name your-domain.com; root /opt/lawcase/frontend/dist; index 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; } location / { try_files $uri $uri/ /index.html; } }这里有几个关键点。第一location /api/会把请求反向代理到后端SpringBoot服务注意proxy_pass末尾的/表示去掉/api前缀直接转发所以后端Controller里的映射不要加/api前缀或者在后端统一加上server.servlet.context-path/api。我个人习惯是在后端配置context-path/apiNginx里直接proxy_pass http://127.0.0.1:8080;不带斜杠这样语义最清晰。第二try_files $uri $uri/ /index.html这条配置是Vue单页应用必须的。因为前端路由是history模式用户直接访问/case/detail/3时Nginx在dist目录下找不到对应的物理文件就会返回404。这个配置会让Nginx在找不到文件时回退到index.html再由Vue路由接管。如果你忘了加这一条刷新页面404会找上门来。也可以用hash模式避免这个问题但URL会带#不太专业所以我坚持history模式Nginx回退。5.4 部署后的验证清单部署完成后不要急着宣布上线。我用一个快速验证清单逐一检查访问域名首页是否正常渲染登录页。登录后菜单是否按角色正确加载。新建一个案件检查列表是否能即时刷新。上传一份PDF附件检查MinIO存储是否成功下载链接是否可访问。查看接口日志确认数据库连接池、Redis连接没有异常报错。刷新一个详情页地址确认不出现404页面白屏。这一套跑下来基本就能交付使用了。6. 常见问题排查与实操心得6.1 跨域请求一被拦截就懵开发环境下前端在http://localhost:5173启动后端在http://localhost:8080直接用axios访问后端接口浏览器会报“Access to XMLHttpRequest at ... has been blocked by CORS policy”。这个问题的解法优先级是先配置Vite代理而不是在后端疯狂加CrossOrigin。因为CrossOrigin在开发时能用但上线后如果前后端不在同源代理撤销后又会失效。正确做法是把跨域问题留给反向代理解决前端始终请求同源地址后端不需要关心CORS。6.2 MyBatis的“动态SQL查不出数据但也不报错”这种情况多半是if条件中的参数名写错了。if testlawyerId ! null里的lawyerId必须和Mapper接口方法入参POJO的属性名一致。还有一个坑是参数为int类型时本来就默认值为0不可能为null所以! null判断永远成立导致条件意外拼接。解决办法是修改POJO字段类型为Integer并在DTO层做好默认值处理。6.3 Vue打包后刷新404的坑这个在上文Nginx配置里已经提到。我见过不少人把锅甩给“前端路由失效”其实问题就是Nginx的try_files没配或者用了File协议直接打开了dist/index.html。此外如果项目部署在子路径比如http://xx.com/lawcase/那么Vue Router需要设置createWebHistory(/lawcase/)同时Nginx的配置也要对应调整否则资源路径全部错乱。6.4 MySQL连接报SSL错误SpringBoot连接MySQL时有可能会遇到The server time zone value Öйú±ê׼ʱ¼ä is unrecognized或者SSL connection error。这类问题大多是因为连接串配置不完整。推荐使用如下jdbc-urlurl: jdbc:mysql://127.0.0.1:3306/law_case_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse不是不加密码而是开发环境可关闭SSL对传输层加密内网部署下性能更好。如果要求等保合规可以启用SSL配置证书但那就不是这个项目要讨论的范畴了。6.5 一些细节避坑经验最后分享几条这几个月维护过程中总结出来的心得都是真实踩坑换来的。一是数据库连接池参数要调。SpringBoot默认的HikariCP参数对于这种小项目够用但连接数上限要注意如果部署在云服务器内存较小的机器上maximum-pool-size建议设置10以内避免连接数过多占满连接导致雪崩。二是案件编号要生成得有规律。我采用的格式是LAS 年月日 当日序号比如LAS20240612001。生成逻辑需要加上数据库锁或分布式ID防止并发下重复。我的做法是在Java里用Redis的INCR命令生成当日自增序号存到Redis里过期时间为24小时既保证唯一也方便读取。三是日志和操作留痕必须有。律所系统的数据涉及律师职业操守问题任何删除、修改操作都要记录操作人、操作时间、操作前后快照。我加了个简单的操作日志拦截器对有Log注解的方法自动记录。这件事虽然短期看不出效果等到出现纠纷时它就是救命稻草。四要是文件上传大小限制要提前设好。SpringBoot默认单个文件上传限制是1MB我实际传一个扫描版PDF就超了。所以必须调整spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB25MB的扫描件在律所很常见如果不调前端上传直接报错用户会以为是系统坏了。五是我的前端打包后一定要在服务器上跑一次构建产物测试。本地npm run build和服务器环境之间偶有差异尤其是依赖版本不一致时容易出现奇奇怪怪的问题。建议直接把dist目录拷到服务器用Nginx跑起来然后用浏览器无痕模式访问一遍关键流程再算正式交付。这套律所案件管理系统做下来最深的感受是技术上并没有太多“黑科技”难点都在业务梳理和细节处理上。案件状态流转不能拍脑袋文件存储不能只想着数据库权限控制要比普通后台系统严格一个等级。后端用SpringBoot把业务边界划清楚前端用Vue把交互体验做顺畅数据库用MySQL把数据根基立稳配合一套标准的部署流程整个链路走下来自然就顺了。如果你也想做一个类似的律所项目建议不要一上来就写代码先花一周把律所的实际业务流程摸透画清楚状态流转图设计好表结构。毕竟源码和部署教程只是启动器真正让系统跑得久、让律师用得顺的永远是背后对业务的理解和对细节的较真。这套项目的完整源码我已经整理好了部署过程遇到的坑也全部记录在部署文档里照着一步步执行大概率能一次跑通。