做律师事务所管理系统这个项目,是我早期接的一个真实需求。当时那边还在用Excel登记案件信息,文书散在各自律师的电脑里,谁手里有几个案子、进展到哪一步了,全靠开会口头汇报。后来我用Spring Boot前后端分离的方式重写了一遍,顺手把源码和文档整理好留给了后来接手的人。这个项目说大不大,但业务链路非常典型:案件、客户、文书、费用、权限,几乎覆盖了管理类系统能遇到的所有常见场景。如果你正准备做毕设,或者想找一个能讲清楚“业务如何落到代码上”的实战项目,这套东西比空看的教程有价值得多。
1. 为什么律师事务所管理系统值得用Spring Boot重写一遍
1.1 律所管理软件的老问题:传统CS架构的痛点
很多律所还在用早期那种C/S架构的本地客户端软件,前端界面是桌面程序,后台数据库单独放在一台Windows服务器上。这种方案在局域网里跑着勉强能用,但问题非常多。
第一是部署成本。新来的律师要装客户端,系统重装后要重新配置数据库连接,版本升级时一台一台机器去更新,IT维护的人得烦死。第二是远程办公基本没法用,律师外出开庭、在家写代理词,根本连不上所里的系统,所有信息只能靠微信和电话来回传。第三是数据安全隐患,Excel文件被随意拷贝,案件信息泄露了也没人知道。我接触的那家律所,还出现过文员误删了整个共享文件夹的证据扫描件,最后靠数据恢复软件抢救回来一部分,过程非常狼狈。
1.2 Spring Boot给这类业务系统带来的价值
Spring Boot的出现,本质上是把后端开发从“繁琐的配置地狱”里解放出来了。做律所管理系统这种业务型项目,核心价值不在技术炫技,而在快速交付、稳定运行、方便维护。用Spring Boot的好处体现在几个非常实在的点上:
- 内置Tomcat,一键启动。打包成一个jar文件,扔到服务器上
java -jar就能跑,不需要单独装Web服务器,律所那种没有专职运维的环境也能轻松部署。 - Starter机制简化依赖管理。引入
spring-boot-starter-web就自带Spring MVC和Jackson,引入mybatis-plus-boot-starter就帮你配好了MyBatis的自动装配,省去大量XML配置。 - 统一配置入口。数据库连接、文件上传路径、JWT密钥,统统写在
application.yml里,改配置不需要重新编译代码。 - 生态成熟、资料多。Spring Boot整合Spring Security、Redis、JWT、MyBatis-Plus、Vue作为前端分离部署,这一整套方案已经被无数项目验证过,遇到问题搜一下就能找到答案。
说句实在话,如果今天还有人拿SSH框架(Struts+Spring+Hibernate)来写这种系统,或者是用那种“JSP+Servlet+JDBC”的原始方式堆代码,我只能说精神可嘉,但确实没必要。Spring Boot + Vue前后端分离,是这个体量项目最合适的组合。
1.3 这套系统适合谁、能学到什么
我整理这套源码和文档时,心里大概想了三类人。
一类是做毕业设计的学生。律所管理系统这种题目在毕设里非常常见,业务清晰、规模适中,既有登录权限这种必经考点,又有案件流转这种能讲出亮点的业务逻辑,答辩时很容易把思路讲清楚。另一类是想转行Java开发、需要项目经验的学习者。看这套源码能搞明白一个真实业务系统是怎么从零到一搭出来的,跟看单元级别的教程完全是两回事。还有一类就是真正需要给律所做信息化的开发者,可以直接在这套代码的基础上改改就能上线。
你能从这个项目里学到的,不是某个孤立的技术点,而是一整条业务系统的构建思路:怎么根据业务角色设计权限模型,怎么用状态字段管理案件流转,怎么设计一套支持全生命周期管理的表结构,这些都是动手写过才知道的东西。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统功能全景:案件、客户、文书与审批的流转逻辑
2.1 案件全生命周期管理:从立案咨询到结案归档
律所的核心资产是案件,所有功能都是围绕案件转的。这个系统里,案件的生命周期被我拆成了五个阶段:意向咨询、已立案、推进中、已结案、已归档。这是整个系统中业务逻辑最密集的地方。
为什么这么拆?因为每个阶段的参与者不一样。意向咨询阶段,前端接待人员录入客户信息和初步诉求,这时候案件还没有编号,只是一个咨询记录。正式立案后,主任或者合伙人要审批,指定承办律师和协办律师,这时候案件才真正“成立”。推进阶段,律师要在这个案件下不断添加工作记录,比如会见当事人、调查取证、提交证据、参加庭审,每一步都有时间戳和操作人。结案后,文书材料归档,费用结算完毕,这个案件才算真正闭环。
这套流程如果用代码实现,核心就是一张t_case表加一张t_case_track操作日志表。案件主表记录当前状态,操作日志表记录每次状态变更的痕迹。这样做有几个好处:任何人打开一个案件,拉一下操作记录,就能完整还原这个案件从进来之后的所有动作;管理者可以分析团队的工作量,谁接了几个案子、每个案子推进了多少步,一目了然。
2.2 客户信息管理:当事人档案与联系人分离
客户这块,很多初学设计的同学会犯一个错误,就是把客户信息直接做成一张“客户表”,字段拉一条龙:姓名、电话、身份证、地址、公司名称、法定代表人、联系人、联系电话。看着很全,实际上业务上根本没法用。
律所的业务对象有两种,个人客户和单位客户。单位客户会有法定代表人、授权委托书上的代理人、具体的对接联系人,这些人可能是不同的自然人。如果都塞在一张表里,当一个公司有多个案件、每个案件对接的人不同时,数据就乱套了。我在这个系统里把客户信息拆成三层:t_client作为客户主体表(个人就是本人,单位就是公司主体),t_client_contact作为联系人表(自然人明细),案件通过外键关联到客户主体,再选择该案件对应的具体联系人。这样设计的好处是,一个公司主体可以挂多个联系人,每个案件可以指定不同的联系人,数据表的关联关系干净清晰。
2.3 文书与证据材料:一个集中式文件库
文书管理是律所系统里最容易被人忽视、实际上最影响体验的模块。律师日常产出的东西全是文档:起诉状、答辩状、代理词、律师函、证据清单、合同文本。这些文件的共同特点是:格式多样(Word、PDF、扫描件)、单个文件大(扫描件动辄几十MB)、命名随意(什么“新建文档.docx”“终版2.docx”满天飞)。
系统的处理方式,是把文件本体放在服务器的指定目录中,数据库里只记录文件的元信息(原始文件名、存储路径、文件大小、上传人、所属案件、上传时间)。上传时用UUID重命名文件,避免中文文件名和重复名带来的各种坑。下载时根据数据库记录的路径去读文件,再用原始文件名输出给用户。这样数据库不会因为存大字段而膨胀,文件备份也简单,直接备份那个目录就行。我在源码里还预留了按案件归类查看文书的功能,律师打开一个案件,就能看到这个案子下所有的材料,不用再去自己的电脑里翻文件夹了。
2.4 费用与收付款:律师费计算的业务细节
费用模块看起来只是一个简单的增删改查,实际上里面藏了不少业务细节。
律师费的计算方式有很多种:按件收费、按时收费、风险代理(打赢了按标的额比例分成)、固定顾问年费。这个系统里我按最简单的折中方案做了:一张t_fee表,记录费用类型(律师费、诉讼费、差旅费、其他)、金额、收费方式、关联案件、收款状态(未收、部分收取、已收齐)。这样满足了绝大多数律所的基础记账需求,又不会把复杂度拉得太高。
这里有个细节值得说一下,就是金额的精度。钱的存储必须用Decimal类型而不是float或double,这一点怎么强调都不过分。浮点数在计算机里是二进制表示的,算出来的结果经常是0.30000000000000004这种,虽然看着只是极小误差,但涉及金额的累计和报表统计时,错误会被不断放大。用BigDecimal虽然操作麻烦一点,但绝对安全。
2.5 角色权限与工作流:管理员、律师、文员各管一摊
权限这块我用的是最经典的RBAC(基于角色的访问控制)模型,没有过度设计,但覆盖了律所的真实岗位分工:
| 角色 | 核心权限 | 业务场景 |
|---|---|---|
| 系统管理员 | 用户管理、全部数据查看 | 维护系统、导出统计报表 |
| 合伙人/主任 | 案件审批、分案指派、全所数据查看 | 审批立案申请、指派承办律师 |
| 主办律师 | 自己名下案件的全部操作 | 录入工作记录、上传文书、结案申请 |
| 协办律师 | 在指派案件下协作操作 | 辅助办案、上传材料 |
| 文员/前台 | 客户录入、案件录入、文书扫描上传 | 案件初始信息登记、材料扫描归档 |
权限控制的落点主要分三个层面。一是接口层面,后端在Spring MVC拦截器里校验JWT携带的角色信息,不匹配的直接返回403。二是菜单层面,前端根据登录用户的角色动态渲染导航菜单,文员登录看不到“审批管理”这个入口。三是数据层面,合伙人能看全所案件,普通律师只能看自己名下参与的案件,这个通过SQL里拼上WHERE条件来实现。
3. 数据库设计:从咨询到结案的表结构拆解
3.1 核心表的划分与关系
我用到的核心表一共九个,关系不算复杂,但每张表都是踩过坑之后调整过的。
t_user:系统用户表,登录账号、密码(BCrypt加密)、角色ID。t_role:角色表。t_client:客户主体表,区分个人客户和单位客户。t_client_contact:联系人表。t_lawyer:律师信息表,执业证号、专业领域、状态等。t_case:案件主表。t_case_track:案件操作记录表。t_file:文件表。t_fee:费用表。
从关联关系上看,t_user和t_lawyer是一对一关系,一个登录账号对应一个律师档案。t_client和t_case是一对多,一个客户可以委托多个案件。t_case和t_case_track是一对多,案件每一次推进都有动态记录。文件表和费用表都通过case_id关联到案件上。这样画出来,整个系统的数据流就非常清晰了。
3.2 case_info案件表的字段设计逻辑
案件主表的字段选择,直接决定了后续开发和统计的方便程度。这张表我当时设计的时候费了点心思,核心字段大概是这样:
sql复制CREATE TABLE `t_case` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`case_no` VARCHAR(32) NOT NULL COMMENT '案件编号',
`title` VARCHAR(200) NOT NULL COMMENT '案件标题',
`client_id` BIGINT NOT NULL COMMENT '客户主体ID',
`contact_id` BIGINT DEFAULT NULL COMMENT '联系人ID',
`case_type` TINYINT NOT NULL COMMENT '案件类型:1民事 2刑事 3行政 4非诉',
`amount_involved` DECIMAL(14,2) DEFAULT NULL COMMENT '涉案标的额',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1意向 2已立案 3推进中 4已结案 5已归档',
`lead_lawyer_id` BIGINT DEFAULT NULL COMMENT '主办律师ID',
`assist_lawyer_id` BIGINT DEFAULT NULL COMMENT '协办律师ID',
`court` VARCHAR(100) DEFAULT NULL COMMENT '受理法院',
`case_closed_time` DATETIME DEFAULT NULL COMMENT '结案时间',
`create_by` VARCHAR(32) DEFAULT NULL COMMENT '创建人',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_case_no` (`case_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='案件信息表';
几个关键点说一下。一是case_no案件编号,不直接用自增ID当编号对外展示,而是用“年份+序号”的规则手动生成,比如2025C001,这样律师和客户沟通时报案件号很正规。二是status状态字段,我用TINYINT数字枚举而不是直接存字符串,存储更精简,也方便在Java里做枚举映射。三是律师字段存的是t_lawyer表的ID,而不是t_user表的ID,因为在业务逻辑里,案件是派给“律师这个职业身份”的,不是派给“登录账号”的。
3.3 状态字段走数值枚举而不是字符串
状态字段的设计,我见过很多项目直接存“进行中”“已结束”这种中文串,或者存“PROCESSING”、“FINISHED”这种英文串。看上去很直观,但实际上是一个隐患。一旦业务上要加一个新状态,字符串类型的字段在代码里散落各地,改起来就是一场灾难。
我的做法是在Java代码里定义枚举类:
java复制public enum CaseStatus {
INTENTION(1, "意向咨询"),
FILED(2, "已立案"),
PROCESSING(3, "推进中"),
CLOSED(4, "已结案"),
ARCHIVED(5, "已归档");
private final int value;
private final String desc;
CaseStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
// getter 省略
public static CaseStatus fromValue(int value) {
for (CaseStatus status : values()) {
if (status.value == value) {
return status;
}
}
throw new IllegalArgumentException("未知的案件状态: " + value);
}
}
数据库里只存数字1/2/3/4/5,展示层通过枚举的desc转为中文。这样状态流转的逻辑都集中在Service层,代码里不会有魔法数字散落各处,后续加状态也只需要改枚举和状态机校验方法两处地方。
3.4 日志与操作留痕:case_track表
律所的业务有个特点:非常讲究留痕和追溯。一个案件的推进过程,将来可能被当事人质疑、被律协检查、被法院调取,每一步都要说得清楚。所以在设计阶段就加上了t_case_track操作记录表:
sql复制CREATE TABLE `t_case_track` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`case_id` BIGINT NOT NULL,
`action_type` TINYINT NOT NULL COMMENT '动作类型:1立案 2指派 3工作记录 4上传文书 5结案 6归档',
`content` VARCHAR(1000) DEFAULT NULL COMMENT '操作内容描述',
`operator_id` BIGINT NOT NULL COMMENT '操作人ID',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_case_id` (`case_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='案件操作记录表';
这张表的逻辑很简单,就是往里面追加记录,只增不改。每次案件状态变化、每次律师录入工作日志、每次上传关键文书,Service层都在同一事务里写一条track记录。这样既保证了业务数据的完整性,又为后续做全所工作量统计、案件效率分析留下了数据基础。
4. 关键模块实操:登录鉴权、案件状态机与文档上传
4.1 JWT登录鉴权的实现思路与细节
登录鉴权用的是Spring Boot生态里最常见的方案:Spring MVC拦截器 + JWT。流程并不复杂,但有几个细节值得说清楚。
用户提交账号密码后,后端用BCrypt去校验密码哈希。校验通过后,生成一个JWT字符串返回给前端。这个Token里我放的信息很克制,只放了userId、username、roleId三个字段。不放多余的业务数据进去,避免Token过期后携带的信息不准确。前端拿到Token后存在本地存储中,之后每次请求在请求头里加Authorization: Bearer <token>。
后端写了一个拦截器,在HandlerInterceptor的preHandle方法里做三件事:解析请求头中的Token、校验签名和过期时间、把解析出的用户信息放进ThreadLocal里的UserContext供后续业务代码获取当前登录用户。这里特别提醒一个坑:JWT密钥一定要放到配置文件中,不要写死在代码里,不然每次改密钥都要重新编译打包。
还有一点,JWT是无状态的,服务端无法主动让某个Token失效。如果要做“强制下线”或者“修改密码后踢掉旧Token”,就得配合Redis存储Token的黑名单或者版本号。我在这套系统里没有上Redis,因为律所管理系统是内部系统,用户量小,Token有效期设了8小时,过期重新登录完全够用,没必要为了一个内部系统引入额外的中间件。
4.2 案件状态机的Service层设计
案件状态流转是这个系统里最容易写乱的地方。新手常见的写法是在Controller里直接写if (status == 1) { status = 2; },然后保存到数据库。这样做短期能跑,但一旦状态分支变多,代码里全是散落的if-else,后面的人根本不敢动。
我的做法是在Service层单独抽一个CaseStatusManager组件,把所有的合法性校验集中到一起。设计思路就是一张“状态转移表”,定义哪些状态下可以执行哪些动作:
java复制@Service
public class CaseStatusManager {
private static final Map<CaseStatus, Set<CaseStatus>> TRANSITIONS = new EnumMap<>(CaseStatus.class);
static {
// 意向咨询 -> 已立案 -> 推进中 -> 已结案 -> 已归档
TRANSITIONS.put(CaseStatus.INTENTION, EnumSet.of(CaseStatus.FILED));
TRANSITIONS.put(CaseStatus.FILED, EnumSet.of(CaseStatus.PROCESSING, CaseStatus.INTENTION));
TRANSITIONS.put(CaseStatus.PROCESSING, EnumSet.of(CaseStatus.CLOSED));
TRANSITIONS.put(CaseStatus.CLOSED, EnumSet.of(CaseStatus.ARCHIVED));
TRANSITIONS.put(CaseStatus.ARCHIVED, EnumSet.noneOf(CaseStatus.class));
}
public void validateTransition(CaseStatus current, CaseStatus target) {
if (!TRANSITIONS.getOrDefault(current, Collections.emptySet()).contains(target)) {
throw new BusinessException("非法的案件状态流转: " + current + " -> " + target);
}
}
}
然后每个状态变更的业务方法里,第一行先调用validateTransition做校验,校验通过后再执行数据更新和track日志写入,整个过程放在一个@Transactional事务里。这样做的最大好处是,业务规则的边界非常清晰,任何一条非法流转都根本无法执行。比如已经归档的案件,任何人都不能再去把它改回“推进中”,这在代码层面就直接堵死了。
4.3 文档上传:本地存储与文件名冲突处理
文件上传这个功能,每个管理系统都会有,但做好细节的不多。我在这套系统里的方案是:本地磁盘存储 + 数据库记录元信息。
先看配置部分:
yaml复制file:
upload-path: /data/law-firm/files
max-size: 100MB
Controller层的MultipartFile接收上传文件后,处理逻辑分三步。第一步,校验文件大小和扩展名,扩展名白名单包括doc/docx/pdf/jpg/png,其他格式一律拒绝,防止有人上传带宏的恶意文档进来。第二步,生成存储文件名,用UUID.randomUUID().toString()加上原文件的扩展名,生成一个类似a8f3c2d4-9f21-4c8b-b1ae-3e10d5f7b2c3.pdf的新名字,按日期分目录存储到/data/law-firm/files/2025/06/下面。第三步,把原始文件名、存储路径、文件大小、关联案件ID、上传人等信息写入t_file表。
这里特别要强调一点,数据库中保存存储名和原始名两个字段。存储名用来定位服务器上的物理文件,原始名用来给用户下载时使用。如果不存原始名,用户下载下来看到的文件名是一串UUID,体验很差;如果直接拿原始名当存储名,两个不同目录下的同名文件就会互相覆盖,而且中文文件名在Linux服务器上还会遇到编码问题。
我在源码里额外做了一个小工具方法,校验上传目录是否存在,不存在就Files.createDirectories()自动创建。这个细节看着不起眼,但在新服务器上部署时能帮你省掉一个“文件上传失败”的排查过程。
5. 部署与源码使用的那些坑
5.1 拿到源码后如何快速跑起来
源码和文档拿到手之后,最怕的就是一脸懵不知道从哪下手。我给你梳理一条最顺的启动路径,照着走就完了。
前置环境只需要三样:JDK 8以上版本、Maven 3.6以上、MySQL 5.7或8.0。我用的是JDK 8加上Spring Boot 2.7版本,如果你用JDK 17跑,大概率会遇到Spring Boot版本不兼容的问题,要么换JDK 8,要么把Spring Boot升到3.x,但升到3.x之后MyBatis-Plus和部分配置类也要跟着升级,不建议新手折腾。
启动分五步走。第一步,用CREATE DATABASE law_firm DEFAULT CHARACTER SET utf8mb4;建库,然后执行项目里sql目录下的init.sql脚本,表结构和初始数据一次性导入。第二步,修改后端application.yml里的数据库账号密码和文件上传路径。第三步,在项目根目录执行mvn clean package -DskipTests,等待打包成功。第四步,java -jar target/law-firm-0.0.1-SNAPSHOT.jar启动后端服务,看到“Started Application”日志说明启动成功。第五步,前端代码是Vue项目,如果只是要测试接口,可以直接用npm run dev启动开发模式,本地访问地址会自动打开登录页。
初始登录账号文档里有写,通常是admin / admin123,第一次登录后建议立刻改密码并更换JWT密钥。
5.2 配置文件的常见坑
配置文件看着简单,实际上是最容易出问题的地方。我把在实际部署中被问到最多的几个坑列一下。
一个是数据库连接串里的时区设置。MySQL 8.0的驱动对时区很敏感,连接串必须加上serverTimezone=Asia/Shanghai,不加会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized这种乱码时区错误,很多人第一次遇到会完全摸不着头脑。
另一个是文件上传路径的权限问题。如果你部署在Linux服务器上,指定的上传目录必须保证运行Java进程的用户有写权限。我见过太多案例,代码在Windows上开发时一切正常,一上Linux就报FileNotFoundException,排查半天发现是目录权限不够,chmod给上权限就解决了。
还有端口冲突。Spring Boot默认跑在8080端口,如果服务器上已经有其他服务占了8080,启动会直接报Port already in use。解决办法是改配置里的server.port,或者启动时加参数覆盖:java -jar app.jar --server.port=8081。
5.3 运行期的隐藏问题
系统跑起来之后还有一类问题,是只有在真实使用过程中才会暴露的。
比较典型的是文件上传大小限制。Spring Boot默认上传单个文件最大1MB,请求体最大10MB。律所扫描的证据材料,一个PDF几十MB是常有的事,不调整配置的话上传一定会失败。解决办法是在配置里加上:
yaml复制spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 150MB
另一个是前端跨域问题。前端Vue跑在5173端口(Vite默认),后端跑在8080端口,两边端口不一致,浏览器会拦截跨域请求。文档里我在后端加了一个全局CORS配置类,允许指定来源访问。如果你换了自己的前端域名,记得同步改CORS配置里的allowedOrigins。
还有一个业务层面的提示:案件数据的统计报表,如果有人凌晨跑大批量查询,可能会慢。表数据量上去后,记得给外键字段和常用查询字段加索引。我在init.sql里已经预留了主要的索引,但如果你在二次开发时加了新的查询条件,别忘了同步加索引,这是新手最容易忽略的性能隐患。
5.4 二次开发建议
如果你准备在这套系统上继续扩展,我给你三个方向上的建议,优先级从高到低排。
第一个是加消息提醒。律所业务里最刚需的功能是开庭提醒和审批提醒。可以在t_hearing表里加开庭时间字段,然后写一个定时任务,每天早上推送给相关律师未来三天的开庭安排。这个功能一旦上线,使用体验立刻提升一大截。
第二个是引入工作流引擎。如果后续审批环节变复杂(比如增加合伙人复核、财务审核等多级审批),建议引入Flowable或者Activity工作流引擎,而不是自己在代码里堆状态机。后期每增加一个审批节点,代码的改动量会指数级上升,工作流引擎的价值在那个时候才会真正体现。
第三个是数据统计可视化。系统上已经积累了案件、费用、工作量数据,用ECharts做几张仪表盘报表,合伙人能看到全所的案件分布、收案趋势、律师工作量排名,这个功能在汇报时非常出彩,也是毕设答辩时的加分项。
我对这套系统的整体评价是:业务完整度足够、代码结构清晰、技术选型保守但实用。花一两天把代码读懂,再按文档跑起来做一轮功能验证,你对“一个真实管理系统该有哪些东西”的理解,会比看十篇教程都深刻。我把自己开发时候的原始思路和踩坑过程都整理进了项目文档,有些细节你读代码时可能不会注意,但读文档时会有种“原来这里这么设计是这个原因”的感觉。做这类项目的经验就是:技术永远只是工具,难点永远在把业务逻辑理清楚、把设计决策想明白。希望这套系统能成为你理解这条思路的一个不错的起点。
