在企事业单位的日常运营中,项目申报管理是个高频且复杂的业务流程。传统纸质申报方式存在三大痛点:首先是流程冗长,一个项目从申报到审批完成往往需要多部门跑动签批;其次是信息不透明,申报人难以实时掌握审批进度;最后是数据统计困难,年终汇总时经常出现数据遗漏或重复计算。
我们团队为某科研机构开发的这套Web申报系统,采用前后端分离架构,实现了三大核心价值:
关键设计决策:选择SpringBoot+Vue技术栈主要考虑团队技术储备与社区生态。SpringBoot的约定优于配置特性可快速搭建稳定后端服务,而Vue的组件化开发模式非常适合构建复杂的表单交互界面。
在项目启动阶段,我们对主流技术方案进行了详细对比:
| 技术方向 | 候选方案 | 最终选择 | 选择理由 |
|---|---|---|---|
| 前端框架 | React/Angular/Vue | Vue 3 + TS | 更轻量级的学习曲线,组合式API更适合表单密集型应用 |
| 后端框架 | SpringBoot/Quarkus | SpringBoot 2.7 | 完善的生态体系,与MyBatis、Spring Security等组件无缝集成 |
| 工作流 | Activiti/Flowable | Flowable 6.3 | 更活跃的社区支持,对SpringBoot的适配更好 |
| 数据库 | MySQL/PostgreSQL | MySQL 8.0 | 团队更熟悉MySQL的优化策略,且项目规模尚未需要PG的高级特性 |
系统采用经典的三层架构,但针对申报业务特点做了特殊优化:
表现层:
业务逻辑层:
数据访问层:
java复制// 审批状态机示例代码
public class ApprovalStateMachine {
private State currentState;
public void submit() {
currentState.handleSubmit(this);
}
public void approve() {
currentState.handleApprove(this);
}
// 状态转换方法
public void transitionTo(State newState) {
this.currentState = newState;
this.currentState.enterState();
}
}
系统包含28张数据表,其中最关键的是项目申报表、用户表和审批记录表的三元关系:
项目申报表(project_application)
用户表(sys_user)
审批记录表(approval_record)
sql复制-- 优化后的项目申报表DDL
CREATE TABLE `project_application` (
`id` bigint NOT NULL COMMENT '雪花算法ID',
`project_name` varchar(100) COLLATE utf8mb4_bin NOT NULL,
`project_code` varchar(32) COLLATE utf8mb4_bin NOT NULL COMMENT '项目编码规则:年+部门+序号',
`applicant_id` bigint NOT NULL,
`budget_amount` decimal(19,4) NOT NULL,
`content` json DEFAULT NULL COMMENT '申报内容JSON',
`attachment_ids` varchar(512) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '附件ID集合',
`current_status` varchar(32) NOT NULL DEFAULT 'DRAFT',
`version` int NOT NULL DEFAULT '0',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_project_code` (`project_code`),
KEY `idx_applicant_status` (`applicant_id`,`current_status`),
FULLTEXT KEY `ft_content` (`project_name`,`content`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
申报系统的核心难点在于不同项目类型需要不同的申报表单。我们开发了基于JSON Schema的动态表单引擎:
json复制{
"title": "科研项目申报表",
"type": "object",
"required": ["projectName", "budget"],
"properties": {
"projectName": {
"type": "string",
"title": "项目名称",
"maxLength": 100
},
"budget": {
"type": "number",
"title": "预算金额(万元)",
"minimum": 1,
"maximum": 1000
}
}
}
mermaid复制graph TD
A[接收表单提交] --> B[Schema校验]
B --> C[数据转换]
C --> D[业务校验]
D --> E[持久化存储]
基于Flowable的工作流引擎,我们设计了多级审批流程:
java复制// 会签审批示例
@Transactional
public void parallelApprove(Long taskId, List<Long> approverIds) {
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
Map<String, Object> variables = new HashMap<>();
variables.put("approvers", approverIds);
variables.put("approvalCount", 0);
runtimeService.setVariables(task.getExecutionId(), variables);
// 为每个审批人创建子任务
approverIds.forEach(approverId -> {
Task subTask = taskService.newTask();
subTask.setName("并行审批");
taskService.saveTask(subTask);
// 设置候选人
taskService.addCandidateUser(subTask.getId(), approverId.toString());
});
}
系统采用改良版的RBAC模型:
java复制public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
Map<String, Object> info = new HashMap<>();
User user = (User) authentication.getPrincipal();
// 添加自定义claim
info.put("deptId", user.getDeptId());
info.put("dataScope", user.getDataScope());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
java复制@Aspect
@Component
public class AuditLogAspect {
@Autowired
private AuditLogService logService;
@AfterReturning(pointcut = "@annotation(auditLog)", returning = "result")
public void afterReturning(JoinPoint joinPoint, AuditLog auditLog, Object result) {
String operation = auditLog.value();
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
AuditLogEntry entry = new AuditLogEntry();
entry.setOperation(operation);
entry.setIp(IpUtils.getIpAddr(request));
entry.setParams(JsonUtils.toJson(joinPoint.getArgs()));
logService.save(entry);
}
}
我们采用Docker Compose实现一键部署:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/conf:/etc/mysql/conf.d
redis:
image: redis:6.2
command: redis-server --appendonly yes
backend:
build: ./backend
ports:
- "8080:8080"
depends_on:
- mysql
- redis
frontend:
build: ./frontend
ports:
- "80:80"
sql复制-- 慢查询优化案例
ALTER TABLE approval_record
ADD INDEX idx_approver_status (approver_id, status);
问题现象:多人同时审批时出现状态不一致
解决方案:
java复制@Transactional
public void approveProject(Long projectId, ApprovalVO vo) {
Project project = projectMapper.selectById(projectId);
if (project.getVersion() != vo.getVersion()) {
throw new OptimisticLockException("版本不一致");
}
// 审批逻辑...
project.setVersion(project.getVersion() + 1);
projectMapper.updateById(project);
}
java复制public boolean approveWithLock(Long projectId) {
String lockKey = "approve_lock:" + projectId;
try {
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(acquired)) {
// 执行业务逻辑
return true;
}
return false;
} finally {
redisTemplate.delete(lockKey);
}
}
问题现象:超过50MB的附件上传时报413错误
优化方案:
nginx复制client_max_body_size 100m;
proxy_read_timeout 300s;
javascript复制async function chunkedUpload(file) {
const chunkSize = 5 * 1024 * 1024; // 5MB
const chunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < chunks; i++) {
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
await axios.post('/upload', chunk, {
headers: {
'Content-Range': `bytes ${i * chunkSize}-${Math.min((i + 1) * chunkSize, file.size)}/${file.size}`
}
});
}
}
当前系统已在三个单位稳定运行半年,日均处理申报200+件。后续演进规划:
经验之谈:在政府类项目申报场景中,要特别注意预留足够的扩展字段。我们的做法是在主表设计20个备用VARCHAR字段和10个备用JSON字段,这在后期对接不同主管部门的定制需求时发挥了关键作用。