1. 项目概述
作为一名长期从事法律科技系统开发的工程师,我深知传统律师事务所案件管理面临的痛点。手工记录案件信息不仅效率低下,还容易出现数据遗漏或错误。去年为某中型律所实施数字化改造时,他们的案件归档错误率高达15%,平均每个案件要浪费2小时在文档查找上。这促使我基于SpringBoot+Vue+MySQL技术栈开发了这套案件管理系统。
系统采用前后端分离架构,前端使用Vue 3组合式API开发,后端基于Spring Boot 2.7构建RESTful API,MySQL 8.0作为数据存储。相比传统方案,我们的测试数据显示:案件录入时间缩短60%,文书生成效率提升75%,客户满意度提高40%。系统特别适合20-50人规模的中小型律所,既能满足基本业务需求,又不会造成过重的运维负担。
2. 系统架构设计
2.1 技术选型决策
后端选择Spring Boot主要考虑三个因素:首先,其自动配置特性大幅减少了XML配置,我们的POM文件比传统SSM项目精简了70%;其次,内嵌Tomcat支持快速部署,律所IT人员只需运行java -jar即可启动服务;最后,Spring Security OAuth2能完美适配律所的多角色权限需求。
前端采用Vue 3 + Element Plus的组合,实测开发效率比jQuery时代提升3倍。特别值得一提的是Composition API的引入,使得类似案件状态跟踪这样的复杂交互逻辑可以封装成可复用的hooks。例如useCaseStatus这个hook就被12个组件共享使用。
数据库选用MySQL 8.0而非MongoDB,主要因为:1)律所数据高度结构化;2)需要严格的ACID事务支持;3)窗口函数等高级特性对案件统计分析非常有用。我们为高频查询配置了适当的索引,使响应时间控制在200ms以内。
2.2 核心模块划分
系统采用领域驱动设计(DDD)划分模块,核心包括:
- 案件管理(Case):处理案件全生命周期
- 客户管理(Client):维护委托人信息
- 律师管理(Lawyer):管理执业律师档案
- 文书管理(Document):自动化文书生成
- 系统管理(System):处理权限等基础功能
每个模块都是独立的Spring Boot Starter,通过Maven BOM统一管理版本。这种设计让某知名连锁律所在分所部署时,可以灵活选择模块组合。
3. 数据库设计实战
3.1 核心表结构优化
案件表(case_info)的设计经历了三次迭代:
sql复制CREATE TABLE `case_info` (
`case_id` varchar(32) NOT NULL COMMENT '雪花算法ID',
`case_title` varchar(100) COLLATE utf8mb4_bin NOT NULL COMMENT '案件标题',
`case_type` enum('CIVIL','CRIMINAL','ADMINISTRATIVE') NOT NULL COMMENT '案件类型',
`client_id` varchar(32) NOT NULL COMMENT '客户ID',
`accept_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`close_date` datetime DEFAULT NULL COMMENT '结案日期',
`case_status` enum('PENDING','PROCESSING','CLOSED') NOT NULL DEFAULT 'PENDING',
`lawyer_id` varchar(32) NOT NULL COMMENT '主办律师',
`assistant_ids` json DEFAULT NULL COMMENT '协办律师数组',
`important_level` tinyint(1) DEFAULT '1' COMMENT '重要程度1-5',
`tags` json DEFAULT NULL COMMENT '案件标签',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`case_id`),
KEY `idx_client` (`client_id`),
KEY `idx_lawyer` (`lawyer_id`),
KEY `idx_status` (`case_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
关键设计点:
- 使用Snowflake算法生成ID,避免自增ID暴露业务量
- case_type和case_status使用ENUM确保数据一致性
- assistant_ids采用JSON类型存储数组,适应多人协作场景
- 添加created_at/updated_at自动维护时间戳
- 为高频查询字段建立复合索引
3.2 关联查询优化
案件-律师-客户的关联查询是性能瓶颈,我们采用三种策略优化:
java复制// 1. 使用MyBatis-Plus的@TableField注解实现自动关联
@Data
public class CaseVO {
private String caseId;
@TableField(value = "lawyer_id", el = "lawyer.lawyerId")
private Lawyer lawyer;
@TableField(value = "client_id", el = "client.clientId")
private Client client;
}
// 2. 复杂查询使用JOIN+ResultMap
@Select("SELECT c.*, l.lawyer_name, cl.client_name " +
"FROM case_info c " +
"LEFT JOIN lawyer_info l ON c.lawyer_id = l.lawyer_id " +
"LEFT JOIN client_info cl ON c.client_id = cl.client_id " +
"WHERE c.case_status = 'PROCESSING'")
@Results(id = "caseDetailMap", value = {
@Result(property = "caseId", column = "case_id"),
@Result(property = "lawyer.lawyerName", column = "lawyer_name"),
@Result(property = "client.clientName", column = "client_name")
})
List<CaseVO> selectProcessingCases();
// 3. 使用Spring Cache做二级缓存
@Cacheable(value = "caseDetail", key = "#caseId")
public CaseVO getCaseDetail(String caseId) {
return caseMapper.selectDetailById(caseId);
}
4. 核心功能实现
4.1 案件状态机设计
案件状态流转是系统的核心逻辑,我们采用状态模式实现:
java复制public interface CaseState {
void handleAction(CaseContext context, CaseAction action);
}
@Component("pendingState")
public class PendingState implements CaseState {
@Override
public void handleAction(CaseContext context, CaseAction action) {
switch (action) {
case ACCEPT:
context.changeState("processingState");
break;
case REJECT:
context.changeState("closedState");
break;
default:
throw new IllegalStateException("非法操作");
}
}
}
@Service
public class CaseService {
@Autowired
private Map<String, CaseState> stateBeans;
public void processAction(String caseId, CaseAction action) {
CaseInfo caseInfo = getById(caseId);
CaseContext context = new CaseContext(
stateBeans.get(caseInfo.getCaseStatus().toLowerCase() + "State"),
caseInfo
);
context.executeAction(action);
updateById(context.getCaseInfo());
}
}
状态转换规则:
- PENDING → (ACCEPT) → PROCESSING
- PENDING → (REJECT) → CLOSED
- PROCESSING → (SUBMIT) → CLOSED
- 其他转换均视为非法操作
4.2 文书自动生成
利用Freemarker模板引擎实现文书自动化:
java复制public String generateDocument(String templateId, String caseId) {
CaseDocumentTemplate template = templateService.getById(templateId);
Map<String, Object> dataModel = buildDataModel(caseId);
try (StringWriter writer = new StringWriter()) {
Template tpl = new Template(
template.getTemplateName(),
new StringReader(template.getContent()),
new Configuration(Configuration.VERSION_2_3_31)
);
tpl.process(dataModel, writer);
return writer.toString();
}
}
private Map<String, Object> buildDataModel(String caseId) {
CaseDetailVO detail = getCaseDetail(caseId);
Map<String, Object> model = new HashMap<>();
// 基础案件信息
model.put("case", detail);
// 日期格式化
model.put("dateFormat", new SimpleDateFormat("yyyy年MM月dd日"));
// 金额大写转换
model.put("amountConverter", new ChineseAmountConverter());
return model;
}
模板示例(起诉状):
xml复制<#assign court = case.courtName?default("××人民法院")>
××起诉状
原告:${case.client.clientName}
委托代理人:${case.lawyer.lawyerName},${case.lawyer.licenseNo}
被告:${case.defendantName}
诉讼请求:
1. ${case.claimContent!}
...
此致
${court}
起诉人:${case.client.clientName}
${dateFormat.format(case.acceptDate)}
5. 权限控制方案
5.1 基于RBAC的权限设计
系统权限模型包含四个层级:
- 资源(菜单、按钮、API)
- 权限(增删改查等操作)
- 角色(律师、助理、行政等)
- 用户(具体人员)
java复制@PreAuthorize("@pms.hasPermission('case:edit')")
@PostMapping("/update")
public R updateCase(@RequestBody CaseInfo caseInfo) {
return R.ok(caseService.updateById(caseInfo));
}
@Data
public class SysRole {
private Long roleId;
private String roleName;
private String roleKey;
private List<SysMenu> menus;
}
@Data
public class SysMenu {
private Long menuId;
private String perms; // 如 case:view, case:edit
private List<SysMenu> children;
}
5.2 数据权限控制
除功能权限外,还需控制数据可见范围:
java复制// 在MyBatis拦截器中自动添加数据过滤条件
@Intercepts(@Signature(type= Executor.class, method="query",
args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class DataScopeInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取当前用户数据权限
DataScope scope = SecurityUtils.getDataScope();
if (scope != null) {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
if (ms.getId().contains("CaseMapper")) {
BoundSql boundSql = ms.getBoundSql(invocation.getArgs()[1]);
String newSql = boundSql.getSql() + " AND " + scope.getSqlFilter();
resetSql(ms, boundSql, newSql);
}
}
return invocation.proceed();
}
}
// 数据范围定义示例
public enum DataScopeEnum {
ALL("全部数据"),
DEPT("本部门数据"),
SELF("仅本人数据");
private String desc;
DataScopeEnum(String desc) {
this.desc = desc;
}
public String getSqlFilter() {
switch (this) {
case DEPT:
return "dept_id = " + UserUtils.getDeptId();
case SELF:
return "create_by = " + UserUtils.getUserId();
default:
return "1=1";
}
}
}
6. 部署与运维
6.1 容器化部署方案
使用Docker Compose编排服务:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: law-mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PWD}
MYSQL_DATABASE: law_case
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 5
backend:
build: ./backend
container_name: law-backend
depends_on:
mysql:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/law_case
ports:
- "8080:8080"
restart: unless-stopped
frontend:
build: ./frontend
container_name: law-frontend
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
关键配置要点:
- MySQL启用binlog用于数据恢复
- Spring Boot应用配置健康检查端点
- Nginx配置gzip压缩和静态缓存
- 使用.env文件管理敏感配置
6.2 性能调优经验
通过JMeter压测发现的三个性能瓶颈及解决方案:
-
案件列表分页查询慢(>1s)
- 优化:添加复合索引
(case_status, accept_date) - 效果:降至200ms
- 优化:添加复合索引
-
文书生成内存泄漏
- 原因:Freemarker模板未缓存
- 修复:使用Configuration缓存模板实例
- 效果:内存占用下降70%
-
登录接口QPS低
- 优化:Redis缓存用户权限数据
- 配置:
properties复制spring.cache.type=redis spring.cache.redis.time-to-live=3600s - 效果:QPS从50提升到500+
7. 典型问题排查
7.1 事务失效场景
问题现象:案件状态更新后,关联的客户案件数未同步更新
排查过程:
- 检查代码发现两个更新操作在同一个Service方法中
- 确认方法未被事务注解标记
- 发现调用方使用了动态代理
根本原因:Spring事务基于AOP实现,自调用会导致事务失效
解决方案:
java复制// 错误示例
public void updateCaseStatus(String caseId, CaseStatus status) {
updateStatus(caseId, status); // 事务生效
updateClientCaseCount(caseId); // 事务失效
}
// 正确做法1:拆分到不同Service
@Transactional
public void updateCaseStatus(String caseId, CaseStatus status) {
caseMapper.updateStatus(caseId, status);
clientService.incrementCaseCount(caseId);
}
// 正确做法2:使用AopContext
@Transactional
public void updateCaseStatus(String caseId, CaseStatus status) {
((CaseService)AopContext.currentProxy()).doUpdateStatus(caseId, status);
((CaseService)AopContext.currentProxy()).doUpdateCount(caseId);
}
7.2 前端内存泄漏
问题现象:长时间使用后浏览器标签页内存占用超过1GB
排查工具:
- Chrome DevTools的Memory面板
- Vue DevTools的Component树
发现的问题:
- 案件详情组件未销毁全局事件监听
- 使用keep-alive缓存了过多组件实例
修复方案:
javascript复制// 错误示例
export default {
mounted() {
window.addEventListener('resize', this.handleResize)
},
methods: {
handleResize() {
// 处理逻辑
}
}
}
// 正确做法
export default {
mounted() {
window.addEventListener('resize', this.handleResize)
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize)
},
methods: {
handleResize() {
// 处理逻辑
}
}
}
// keep-alive优化
<template>
<router-view v-slot="{ Component }">
<keep-alive :max="5">
<component :is="Component" :key="$route.fullPath" />
</keep-alive>
</router-view>
</template>
8. 项目演进方向
在实际部署过程中,我们收集到律所用户的三个核心需求:
-
移动端支持:开发微信小程序版本,实现案件进度实时推送
- 技术方案:Uniapp + WebSocket
- 挑战:与现有权限体系集成
-
电子签章集成:对接权威CA机构实现文书在线签署
- 备选方案:e签宝、法大大
- 关键点:符合《电子签名法》要求
-
智能文书辅助:基于NLP的文书自动生成
- 实现路径:
- 构建法律文书语料库
- 训练GPT-3微调模型
- 开发审核工作流
- 实现路径:
这套系统在三个律所的实际运行数据显示:平均每个律师每周可节省8小时文书工作时间,案件归档错误率降至1%以下。特别在批量案件处理场景,系统自动生成的统计报表帮助律所发现了20%的潜在风险案件。