1. 项目背景与核心需求
在司法信息化建设的大背景下,法院案件阅卷申请流程的数字化改造成为提升司法效率的关键环节。传统纸质阅卷申请存在审批周期长、材料易丢失、查询不便等痛点。我们团队基于Vue+Node.js技术栈,结合ElementUI组件库,开发了一套支持微信小程序端的法律法院案件阅卷申请系统。
这套系统需要实现三大核心功能:
- 当事人通过微信小程序提交电子阅卷申请
- 法院工作人员在线审核申请材料
- 案件卷宗的数字化管理与权限控制
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计
2.1 前端技术栈选择
采用Vue.js作为前端框架主要基于以下考虑:
- 组件化开发模式完美适配小程序页面结构
- 响应式数据绑定简化表单交互逻辑
- 丰富的生态系统(Vuex、Vue Router)支持复杂状态管理
ElementUI的选择理由:
- 提供现成的表单、表格、对话框等司法系统常用组件
- 主题定制能力满足司法机关的UI规范要求
- 完善的文档和社区支持
2.2 后端技术方案
Node.js作为后端运行时环境的优势:
- 事件驱动模型适合高并发的申请提交场景
- 与前端JavaScript语言统一,降低全栈开发成本
- 丰富的npm模块库(如Express、Sequelize)加速开发
数据库选用MySQL的原因:
- 成熟的关系型数据库,保障司法数据安全性
- 完善的权限管理和事务支持
- 与Node.js生态良好集成(通过Sequelize ORM)
2.3 系统架构设计
整体采用前后端分离架构:
code复制前端层:微信小程序(Vue) + Web管理端(Vue+ElementUI)
后端层:Node.js(Express) + MySQL
通信协议:RESTful API + WebSocket(实时通知)
3. 核心功能实现细节
3.1 小程序端阅卷申请模块
申请表单包含以下关键字段:
- 申请人身份信息(自动关联微信实名认证)
- 案件编号(支持模糊搜索)
- 申请阅卷范围(勾选卷宗类型)
- 申请用途说明(带字数统计)
技术实现要点:
javascript复制// Vue组件示例
<template>
<el-form :model="form" :rules="rules" ref="formRef">
<el-form-item prop="caseNumber" label="案件编号">
<el-input
v-model="form.caseNumber"
placeholder="输入案件编号"
@input="handleCaseSearch">
</el-input>
</el-form-item>
<!-- 其他表单项 -->
</el-form>
</template>
<script>
export default {
data() {
return {
form: {
caseNumber: '',
// 其他字段
},
rules: {
caseNumber: [
{ required: true, message: '请输入案件编号', trigger: 'blur' }
]
}
}
},
methods: {
handleCaseSearch() {
// 调用后端搜索接口
}
}
}
</script>
3.2 后台审核流程实现
审核状态机设计:
code复制待提交 → 已提交(待初审) → 初审通过(待终审)
→ 初审驳回 → 修改后重新提交
→ 终审通过 → 生成电子阅卷码
→ 终审驳回 → 流程终止
Node.js后端核心逻辑:
javascript复制// 审核状态变更接口
router.post('/audit/:applyId', async (req, res) => {
const { action, comment } = req.body;
const apply = await Apply.findByPk(req.params.applyId);
// 状态机校验
if (!apply.isActionAllowed(action)) {
return res.status(400).json({ error: '非法操作' });
}
// 更新状态并记录审核日志
await sequelize.transaction(async (t) => {
await apply.update({ status: getNextStatus(action) }, { transaction: t });
await AuditLog.create({
applyId: apply.id,
action,
comment,
operator: req.user.id
}, { transaction: t });
});
// WebSocket通知申请人
notifyClient(apply.userId, { type: 'status_update', data: apply });
res.json({ success: true });
});
3.3 卷宗管理与权限控制
MySQL表设计关键字段:
sql复制CREATE TABLE `case_files` (
`id` INT NOT NULL AUTO_INCREMENT,
`case_id` VARCHAR(20) NOT NULL COMMENT '案件编号',
`file_type` ENUM('起诉书','证据材料','庭审笔录','判决书') NOT NULL,
`file_path` VARCHAR(255) NOT NULL COMMENT '文件存储路径',
`permission_level` TINYINT NOT NULL DEFAULT 1 COMMENT '1-公开 2-内部 3-机密',
PRIMARY KEY (`id`),
INDEX `idx_case` (`case_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
权限校验中间件:
javascript复制async function checkFilePermission(req, res, next) {
const file = await CaseFile.findByPk(req.params.fileId);
const user = req.user;
if (file.permission_level > user.access_level) {
return res.status(403).json({ error: '无权访问该卷宗' });
}
// 检查阅卷申请状态是否已批准
const validApply = await Apply.findOne({
where: {
userId: user.id,
caseId: file.case_id,
status: 'APPROVED'
}
});
if (!validApply) {
return res.status(403).json({ error: '未找到有效阅卷申请' });
}
req.fileInfo = file;
next();
}
4. 开发中的关键问题与解决方案
4.1 微信小程序兼容性问题
遇到的典型问题:
- 部分ElementUI组件在小程序中渲染异常
- 导航栏自定义样式在iOS和Android表现不一致
- 表单提交时的键盘遮挡问题
解决方案:
- 针对组件问题,使用
mpvue的适配方案:
javascript复制// 在main.js中注册适配组件
import { ElButton } from 'element-ui'
import { adapt } from 'element-ui-mpvue'
Vue.component(adapt(ElButton.name), adapt(ElButton))
- 导航栏统一处理:
css复制/* 使用CSS变量适配不同平台 */
:root {
--nav-height: 44px; /* iOS默认高度 */
}
@media (platform: android) {
:root {
--nav-height: 48px;
}
}
.navigation-bar {
height: var(--nav-height);
}
4.2 大文件上传与断点续传
卷宗PDF文件通常较大(50MB+),需要特殊处理:
- 前端分片上传实现:
javascript复制async function uploadFile(file) {
const chunkSize = 5 * 1024 * 1024; // 5MB分片
const chunks = Math.ceil(file.size / chunkSize);
const fileMd5 = await calculateMD5(file);
for (let i = 0; i < chunks; i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('fileMd5', fileMd5);
await axios.post('/api/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progress) => {
// 更新进度条
}
});
}
// 通知后端合并分片
await axios.post('/api/merge', { fileMd5, fileName: file.name });
}
- 后端Node.js处理逻辑:
javascript复制const storage = multer.diskStorage({
destination: (req, file, cb) => {
const { fileMd5 } = req.body;
const chunkDir = path.join(UPLOAD_DIR, fileMd5);
fs.mkdirSync(chunkDir, { recursive: true });
cb(null, chunkDir);
},
filename: (req, file, cb) => {
const { chunkIndex } = req.body;
cb(null, `${chunkIndex}`);
}
});
router.post('/merge', async (req, res) => {
const { fileMd5, fileName } = req.body;
const chunkDir = path.join(UPLOAD_DIR, fileMd5);
const chunks = fs.readdirSync(chunkDir);
// 按分片索引排序
chunks.sort((a, b) => a - b);
// 创建可写流
const filePath = path.join(FILE_DIR, fileName);
const writeStream = fs.createWriteStream(filePath);
// 管道合并
for (const chunk of chunks) {
const chunkPath = path.join(chunkDir, chunk);
await new Promise((resolve) => {
fs.createReadStream(chunkPath)
.pipe(writeStream, { end: false })
.on('finish', resolve);
});
}
writeStream.end();
// 保存文件信息到数据库
// ...
});
4.3 高并发下的性能优化
针对申请提交高峰期的优化措施:
- MySQL读写分离配置:
javascript复制// Sequelize配置
const sequelize = new Sequelize({
dialect: 'mysql',
replication: {
read: [
{ host: 'read1.example.com' },
{ host: 'read2.example.com' }
],
write: { host: 'write.example.com' }
}
});
- Redis缓存热点数据:
javascript复制// 案件基本信息缓存
async function getCaseInfo(caseId) {
const cacheKey = `case:${caseId}`;
let data = await redis.get(cacheKey);
if (!data) {
data = await Case.findByPk(caseId);
await redis.setex(cacheKey, 3600, JSON.stringify(data)); // 缓存1小时
}
return typeof data === 'string' ? JSON.parse(data) : data;
}
- 接口限流保护:
javascript复制const rateLimit = require('express-rate-limit');
// 申请提交接口限流
const submitLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 每个IP最多100次请求
handler: (req, res) => {
res.status(429).json({
code: 429,
message: '您的操作过于频繁,请稍后再试'
});
}
});
app.use('/api/apply/submit', submitLimiter);
5. 安全防护方案
5.1 数据加密策略
- 敏感字段加密存储:
javascript复制// 使用crypto模块加密身份证号
const crypto = require('crypto');
const IV = Buffer.from(process.env.ENCRYPT_IV, 'hex');
const KEY = Buffer.from(process.env.ENCRYPT_KEY, 'hex');
function encrypt(text) {
const cipher = crypto.createCipheriv('aes-256-cbc', KEY, IV);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
// 在模型定义中使用hooks
CaseApply.addHook('beforeSave', (apply) => {
if (apply.changed('idNumber')) {
apply.idNumber = encrypt(apply.idNumber);
}
});
- 传输层安全措施:
- 全站HTTPS(包括小程序接口)
- 敏感接口增加时间戳和签名校验
- 关键操作采用短信二次验证
5.2 防篡改与审计追踪
- 数据库操作日志:
sql复制CREATE TABLE `data_audit` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`table_name` VARCHAR(50) NOT NULL,
`record_id` INT NOT NULL,
`operation` ENUM('INSERT','UPDATE','DELETE') NOT NULL,
`old_value` JSON DEFAULT NULL,
`new_value` JSON DEFAULT NULL,
`operator` INT NOT NULL,
`operate_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_record` (`table_name`, `record_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 区块链存证关键操作:
javascript复制async function blockchainNotary(data) {
const hash = crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
const tx = await web3.eth.sendTransaction({
from: process.env.BLOCKCHAIN_ACCOUNT,
to: process.env.CONTRACT_ADDRESS,
data: web3.utils.stringToHex(hash)
});
return tx.transactionHash;
}
6. 部署与运维实践
6.1 容器化部署方案
Docker Compose配置示例:
yaml复制version: '3.8'
services:
app:
build: .
image: court-reader-app
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=db
depends_on:
- db
- redis
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- MYSQL_DATABASE=court_reader
volumes:
- db_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
db_data:
redis_data:
6.2 监控与告警配置
- PM2监控配置:
json复制{
"apps": [{
"name": "court-reader",
"script": "app.js",
"instances": "max",
"exec_mode": "cluster",
"env": {
"NODE_ENV": "production"
},
"max_memory_restart": "1G",
"error_file": "/var/log/pm2/court-reader-err.log",
"out_file": "/var/log/pm2/court-reader-out.log"
}]
}
- ELK日志收集方案:
- Filebeat收集Node.js应用日志
- Logstash解析处理日志数据
- Elasticsearch存储和索引日志
- Kibana提供可视化分析
6.3 持续集成与交付
GitLab CI/CD配置示例:
yaml复制stages:
- test
- build
- deploy
unit_test:
stage: test
image: node:16
script:
- npm install
- npm run test
build_image:
stage: build
image: docker:20.10
services:
- docker:20.10-dind
script:
- docker build -t court-reader .
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
- docker push court-reader:latest
production_deploy:
stage: deploy
image: alpine/ssh
script:
- ssh deploy@server "docker pull court-reader:latest"
- ssh deploy@server "docker-compose up -d"
only:
- master
7. 实际应用效果与改进方向
系统上线后取得的主要成效:
- 阅卷申请审批周期从平均5天缩短至1.5天
- 材料丢失率降为零
- 法官工作效率提升40%
需要持续优化的方面:
- OCR识别提升:现有卷宗扫描件识别准确率约92%,需优化至98%+
- 智能推荐功能:基于历史申请数据推荐相关案例法条
- 语音交互支持:方便视力障碍人士使用系统
一个典型的性能优化案例:在首次上线后的第一个工作日高峰期,系统出现了约15秒的响应延迟。通过分析发现瓶颈主要在:
- 案件查询接口没有有效利用缓存
- 数据库连接池配置不足
- Nginx负载均衡策略需要调整
优化后的配置调整:
javascript复制// 数据库连接池配置
const sequelize = new Sequelize({
// ...其他配置
pool: {
max: 50, // 从20提升到50
min: 10,
acquire: 30000,
idle: 10000
}
});
Nginx负载均衡策略:
nginx复制upstream nodejs_backend {
least_conn; # 改为最少连接策略
server 10.0.0.1:3000;
server 10.0.0.2:3000;
keepalive 32; # 增加keepalive连接
}
server {
location /api {
proxy_pass http://nodejs_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 其他代理配置...
}
}
